From 297c22d832ddd0de93e6031b5cada6e376c8715a Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Tue, 29 Apr 2025 22:27:24 +0000 Subject: [PATCH] Run rustfmt --- src/api/album.rs | 270 ++++++++------ src/api/albums.rs | 38 +- src/api/artists.rs | 227 +++++++----- src/api/auth.rs | 194 +++++----- src/api/history.rs | 51 +-- src/api/mod.rs | 8 +- src/api/profile.rs | 537 ++++++++++++++++------------ src/api/search.rs | 107 +++--- src/api/songs.rs | 235 +++++++----- src/api/upload.rs | 497 ++++++++++++++----------- src/api/users.rs | 203 ++++++----- src/app.rs | 36 +- src/components/add_album.rs | 40 +-- src/components/add_artist.rs | 20 +- src/components/dashboard_row.rs | 133 +++---- src/components/dashboard_tile.rs | 16 +- src/components/error.rs | 58 ++- src/components/error_template.rs | 2 +- src/components/loading.rs | 28 +- src/components/menu.rs | 19 +- src/components/mod.rs | 24 +- src/components/personal.rs | 75 ++-- src/components/playbar.rs | 324 +++++++++-------- src/components/queue.rs | 214 +++++------ src/components/sidebar.rs | 8 +- src/components/song.rs | 20 +- src/components/song_list.rs | 456 ++++++++++++----------- src/components/upload.rs | 434 +++++++++++----------- src/components/upload_dropdown.rs | 23 +- src/lib.rs | 15 +- src/main.rs | 46 ++- src/models/backend/album.rs | 29 +- src/models/backend/artist.rs | 55 +-- src/models/backend/history_entry.rs | 24 +- src/models/backend/playlist.rs | 32 +- src/models/backend/song.rs | 46 +-- src/models/backend/user.rs | 459 ++++++++++++++---------- src/models/frontend/album.rs | 44 +-- src/models/frontend/artist.rs | 34 +- src/models/frontend/playstatus.rs | 62 ++-- src/models/frontend/song.rs | 127 +++---- src/pages/album.rs | 29 +- src/pages/artist.rs | 55 +-- src/pages/login.rs | 10 +- src/pages/mod.rs | 10 +- src/pages/profile.rs | 504 +++++++++++++------------- src/pages/search.rs | 2 +- src/pages/signup.rs | 6 +- src/pages/song.rs | 23 +- src/util/audio.rs | 47 +-- src/util/auth_backend.rs | 46 +-- src/util/database.rs | 25 +- src/util/fileserv.rs | 34 +- src/util/mod.rs | 14 +- src/util/require_auth.rs | 63 ++-- src/util/state.rs | 33 +- 56 files changed, 3390 insertions(+), 2781 deletions(-) diff --git a/src/api/album.rs b/src/api/album.rs index a1d0ccf..96e87b1 100644 --- a/src/api/album.rs +++ b/src/api/album.rs @@ -1,141 +1,181 @@ -use leptos::prelude::*; use crate::models::frontend; +use leptos::prelude::*; 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; - } + 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")] pub async fn get_album(id: i32) -> Result, ServerFnError> { - use crate::models::backend::Album; - use crate::schema::*; + use crate::models::backend::Album; + use crate::schema::*; - let db_con = &mut get_db_conn(); + let db_con = &mut get_db_conn(); - let album = albums::table - .find(id) - .first::(db_con) - .optional() - .map_err(|e| ServerFnError::::ServerError(format!("Error getting album: {e}")))?; + let album = albums::table + .find(id) + .first::(db_con) + .optional() + .map_err(|e| { + ServerFnError::::ServerError(format!("Error getting album: {e}")) + })?; - let Some(album) = album else { return Ok(None) }; + let Some(album) = album else { return Ok(None) }; - let artists: Vec = album_artists::table - .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)?; + let artists: Vec = album_artists::table + .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)?; - let img = album.image_path.unwrap_or("/assets/images/placeholders/MusicPlaceholder.svg".to_string()); + let img = album + .image_path + .unwrap_or("/assets/images/placeholders/MusicPlaceholder.svg".to_string()); - let album = frontend::Album { - id: album.id.unwrap(), - title: album.title, - artists, - release_date: album.release_date, - image_path: img - }; + let album = frontend::Album { + id: album.id.unwrap(), + title: album.title, + artists, + release_date: album.release_date, + image_path: img, + }; - Ok(Some(album)) + Ok(Some(album)) } #[server(endpoint = "album/get_songs")] pub async fn get_songs(id: i32) -> Result, ServerFnError> { - use std::collections::HashMap; - use crate::api::auth::get_logged_in_user; - use crate::schema::*; + 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?; - let db_con = &mut get_db_conn(); - - let song_list = if let Some(user) = user { - let user_id = user.id.unwrap(); - let song_list: Vec<(backend::Album, Option, Option, Option<(i32, i32)>, Option<(i32, i32)>)> = - albums::table - .find(id) - .left_join(songs::table.on(albums::id.nullable().eq(songs::album_id))) - .left_join(song_artists::table.inner_join(artists::table).on(songs::id.eq(song_artists::song_id))) - .left_join(song_likes::table.on(songs::id.eq(song_likes::song_id).and(song_likes::user_id.eq(user_id)))) - .left_join(song_dislikes::table.on(songs::id.eq(song_dislikes::song_id).and(song_dislikes::user_id.eq(user_id)))) - .select(( - albums::all_columns, - songs::all_columns.nullable(), - artists::all_columns.nullable(), - song_likes::all_columns.nullable(), - song_dislikes::all_columns.nullable() - )) - .order(songs::track.asc()) - .load(db_con)?; - song_list - } else { - let song_list: Vec<(backend::Album, Option, Option)> = - albums::table - .find(id) - .left_join(songs::table.on(albums::id.nullable().eq(songs::album_id))) - .left_join(song_artists::table.inner_join(artists::table).on(songs::id.eq(song_artists::song_id))) - .select(( - albums::all_columns, - songs::all_columns.nullable(), - artists::all_columns.nullable() - )) - .order(songs::track.asc()) - .load(db_con)?; + let db_con = &mut get_db_conn(); - let song_list: Vec<(backend::Album, Option, Option, Option<(i32, i32)>, Option<(i32, i32)>)> = - song_list.into_iter().map( |(album, song, artist)| (album, song, artist, None, None) ).collect(); - song_list - }; + let song_list = if let Some(user) = user { + let user_id = user.id.unwrap(); + let song_list: Vec<( + backend::Album, + Option, + Option, + Option<(i32, i32)>, + Option<(i32, i32)>, + )> = albums::table + .find(id) + .left_join(songs::table.on(albums::id.nullable().eq(songs::album_id))) + .left_join( + song_artists::table + .inner_join(artists::table) + .on(songs::id.eq(song_artists::song_id)), + ) + .left_join( + song_likes::table.on(songs::id + .eq(song_likes::song_id) + .and(song_likes::user_id.eq(user_id))), + ) + .left_join( + song_dislikes::table.on(songs::id + .eq(song_dislikes::song_id) + .and(song_dislikes::user_id.eq(user_id))), + ) + .select(( + albums::all_columns, + songs::all_columns.nullable(), + artists::all_columns.nullable(), + song_likes::all_columns.nullable(), + song_dislikes::all_columns.nullable(), + )) + .order(songs::track.asc()) + .load(db_con)?; + song_list + } else { + let song_list: Vec<( + backend::Album, + Option, + Option, + )> = albums::table + .find(id) + .left_join(songs::table.on(albums::id.nullable().eq(songs::album_id))) + .left_join( + song_artists::table + .inner_join(artists::table) + .on(songs::id.eq(song_artists::song_id)), + ) + .select(( + albums::all_columns, + songs::all_columns.nullable(), + artists::all_columns.nullable(), + )) + .order(songs::track.asc()) + .load(db_con)?; - let mut album_songs: HashMap = HashMap::with_capacity(song_list.len()); - - for (album, song, artist, like, dislike) in song_list { - if let Some(song) = song { - if let Some(stored_songdata) = album_songs.get_mut(&song.id.unwrap()) { - // If the song is already in the map, update the artists - if let Some(artist) = artist { - stored_songdata.artists.push(artist); - } - } else { - let like_dislike = match (like, dislike) { - (Some(_), Some(_)) => Some((true, true)), - (Some(_), None) => Some((true, false)), - (None, Some(_)) => Some((false, true)), - _ => None, - }; - - let image_path = song.image_path.unwrap_or( - album.image_path.clone().unwrap_or("/assets/images/placeholders/MusicPlaceholder.svg".to_string())); - - let songdata = frontend::Song { - id: song.id.unwrap(), - title: song.title, - artists: artist.map(|artist| vec![artist]).unwrap_or_default(), - album: Some(album), - track: song.track, - duration: song.duration, - release_date: song.release_date, - song_path: song.storage_path, - image_path, - like_dislike, - added_date: song.added_date.unwrap(), - }; - - album_songs.insert(song.id.unwrap(), songdata); - } - } - } - - // Sort the songs by date - let mut songs: Vec = album_songs.into_values().collect(); - songs.sort_by(|a, b| a.track.cmp(&b.track)); + let song_list: Vec<( + backend::Album, + Option, + Option, + Option<(i32, i32)>, + Option<(i32, i32)>, + )> = song_list + .into_iter() + .map(|(album, song, artist)| (album, song, artist, None, None)) + .collect(); + song_list + }; - Ok(songs) + let mut album_songs: HashMap = HashMap::with_capacity(song_list.len()); + + for (album, song, artist, like, dislike) in song_list { + if let Some(song) = song { + if let Some(stored_songdata) = album_songs.get_mut(&song.id.unwrap()) { + // If the song is already in the map, update the artists + if let Some(artist) = artist { + stored_songdata.artists.push(artist); + } + } else { + let like_dislike = match (like, dislike) { + (Some(_), Some(_)) => Some((true, true)), + (Some(_), None) => Some((true, false)), + (None, Some(_)) => Some((false, true)), + _ => None, + }; + + let image_path = song.image_path.unwrap_or( + album + .image_path + .clone() + .unwrap_or("/assets/images/placeholders/MusicPlaceholder.svg".to_string()), + ); + + let songdata = frontend::Song { + id: song.id.unwrap(), + title: song.title, + artists: artist.map(|artist| vec![artist]).unwrap_or_default(), + album: Some(album), + track: song.track, + duration: song.duration, + release_date: song.release_date, + song_path: song.storage_path, + image_path, + like_dislike, + added_date: song.added_date.unwrap(), + }; + + album_songs.insert(song.id.unwrap(), songdata); + } + } + } + + // Sort the songs by date + let mut songs: Vec = album_songs.into_values().collect(); + songs.sort_by(|a, b| a.track.cmp(&b.track)); + + Ok(songs) } diff --git a/src/api/albums.rs b/src/api/albums.rs index f7e72b6..b1cc376 100644 --- a/src/api/albums.rs +++ b/src/api/albums.rs @@ -11,46 +11,54 @@ cfg_if! { } /// Add an album to the database -/// +/// /// # Arguments -/// +/// /// * `album_title` - The name of the artist to add /// * `release_data` - The release date of the album (Optional) /// * `image_path` - The path to the album's image file (Optional) -/// +/// /// # Returns /// * `Result<(), Box>` - A empty result if successful, or an error -/// +/// #[server(endpoint = "albums/add-album")] -pub async fn add_album(album_title: String, release_date: Option, image_path: Option) -> Result<(), ServerFnError> { - use crate::schema::albums::{self}; +pub async fn add_album( + album_title: String, + release_date: Option, + image_path: Option, +) -> Result<(), ServerFnError> { use crate::models::backend::Album; + 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())) + 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(), + )) } }, - None => None + None => None, }; let image_path_arg = image_path.filter(|image_path| !image_path.is_empty()); - + let new_album = Album { id: None, title: album_title, release_date: parsed_release_date, - image_path: image_path_arg + image_path: image_path_arg, }; let db = &mut get_db_conn(); diesel::insert_into(albums::table) .values(&new_album) .execute(db) - .map_err(|e| ServerFnError::::ServerError(format!("Error adding album: {e}")))?; + .map_err(|e| { + ServerFnError::::ServerError(format!("Error adding album: {e}")) + })?; Ok(()) } diff --git a/src/api/artists.rs b/src/api/artists.rs index 28ccf41..9ca7599 100644 --- a/src/api/artists.rs +++ b/src/api/artists.rs @@ -2,8 +2,8 @@ use leptos::prelude::*; use cfg_if::cfg_if; -use crate::models::frontend; use crate::models::backend::Artist; +use crate::models::frontend; cfg_if! { if #[cfg(feature = "ssr")] { @@ -16,14 +16,14 @@ cfg_if! { } /// Add an artist to the database -/// +/// /// # Arguments -/// +/// /// * `artist_name` - The name of the artist to add -/// +/// /// # Returns /// * `Result<(), Box>` - A empty result if successful, or an error -/// +/// #[server(endpoint = "artists/add-artist")] pub async fn add_artist(artist_name: String) -> Result<(), ServerFnError> { use crate::schema::artists::dsl::*; @@ -38,8 +38,10 @@ 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}")))?; - + .map_err(|e| { + ServerFnError::::ServerError(format!("Error adding artist: {e}")) + })?; + Ok(()) } @@ -53,107 +55,140 @@ pub async fn get_artist_by_id(artist_id: i32) -> Result, ServerFn .filter(id.eq(artist_id)) .first::(db) .optional() - .map_err(|e| ServerFnError::::ServerError(format!("Error getting artist: {e}")))?; + .map_err(|e| { + ServerFnError::::ServerError(format!("Error getting artist: {e}")) + })?; Ok(artist) } #[server(endpoint = "artists/top_songs")] -pub async fn top_songs_by_artist(artist_id: i32, limit: Option) -> Result, ServerFnError> { - use crate::models::backend::Song; +pub async fn top_songs_by_artist( + artist_id: i32, + limit: Option, +) -> Result, ServerFnError> { 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.unwrap(); + let user_id = get_user() + .await + .map_err(|e| { + ServerFnError::ServerError::(format!("Error getting user: {e}")) + })? + .id + .unwrap(); let db = &mut get_db_conn(); - let song_play_counts: Vec<(i32, i64)> = - if let Some(limit) = limit { - song_history::table - .group_by(song_history::song_id) - .select((song_history::song_id, diesel::dsl::count(song_history::id))) - .left_join(song_artists::table.on(song_artists::song_id.eq(song_history::song_id))) - .filter(song_artists::artist_id.eq(artist_id)) - .order_by(diesel::dsl::count(song_history::id).desc()) - .left_join(songs::table.on(songs::id.eq(song_history::song_id))) - .limit(limit) - .load(db)? - } else { - song_history::table - .group_by(song_history::song_id) - .select((song_history::song_id, diesel::dsl::count(song_history::id))) - .left_join(song_artists::table.on(song_artists::song_id.eq(song_history::song_id))) - .filter(song_artists::artist_id.eq(artist_id)) - .order_by(diesel::dsl::count(song_history::id).desc()) - .left_join(songs::table.on(songs::id.eq(song_history::song_id))) - .load(db)? - }; + let song_play_counts: Vec<(i32, i64)> = if let Some(limit) = limit { + song_history::table + .group_by(song_history::song_id) + .select((song_history::song_id, diesel::dsl::count(song_history::id))) + .left_join(song_artists::table.on(song_artists::song_id.eq(song_history::song_id))) + .filter(song_artists::artist_id.eq(artist_id)) + .order_by(diesel::dsl::count(song_history::id).desc()) + .left_join(songs::table.on(songs::id.eq(song_history::song_id))) + .limit(limit) + .load(db)? + } else { + song_history::table + .group_by(song_history::song_id) + .select((song_history::song_id, diesel::dsl::count(song_history::id))) + .left_join(song_artists::table.on(song_artists::song_id.eq(song_history::song_id))) + .filter(song_artists::artist_id.eq(artist_id)) + .order_by(diesel::dsl::count(song_history::id).desc()) + .left_join(songs::table.on(songs::id.eq(song_history::song_id))) + .load(db)? + }; let song_play_counts: HashMap = song_play_counts.into_iter().collect(); let top_song_ids: Vec = song_play_counts.keys().copied().collect(); - let top_songs: Vec<(Song, Option, Option, Option<(i32, i32)>, Option<(i32, i32)>)> - = songs::table + let top_songs: Vec<( + Song, + Option, + Option, + Option<(i32, i32)>, + Option<(i32, i32)>, + )> = songs::table .filter(songs::id.eq_any(top_song_ids)) - .left_join(albums::table.on(songs::album_id.eq(albums::id.nullable()))) - .left_join(song_artists::table.inner_join(artists::table).on(songs::id.eq(song_artists::song_id))) - .left_join(song_likes::table.on(songs::id.eq(song_likes::song_id).and(song_likes::user_id.eq(user_id)))) - .left_join(song_dislikes::table.on( - songs::id.eq(song_dislikes::song_id).and(song_dislikes::user_id.eq(user_id)))) - .select(( - songs::all_columns, - albums::all_columns.nullable(), - artists::all_columns.nullable(), - song_likes::all_columns.nullable(), - song_dislikes::all_columns.nullable(), - )) - .load(db)?; + .left_join(albums::table.on(songs::album_id.eq(albums::id.nullable()))) + .left_join( + song_artists::table + .inner_join(artists::table) + .on(songs::id.eq(song_artists::song_id)), + ) + .left_join( + song_likes::table.on(songs::id + .eq(song_likes::song_id) + .and(song_likes::user_id.eq(user_id))), + ) + .left_join( + song_dislikes::table.on(songs::id + .eq(song_dislikes::song_id) + .and(song_dislikes::user_id.eq(user_id))), + ) + .select(( + songs::all_columns, + albums::all_columns.nullable(), + artists::all_columns.nullable(), + song_likes::all_columns.nullable(), + song_dislikes::all_columns.nullable(), + )) + .load(db)?; - let mut top_songs_map: HashMap = HashMap::with_capacity(top_songs.len()); + let mut top_songs_map: HashMap = + HashMap::with_capacity(top_songs.len()); - for (song, album, artist, like, dislike) in top_songs { - let song_id = song.id - .ok_or(ServerFnError::ServerError::("Song id not found in database".to_string()))?; + for (song, album, artist, like, dislike) in top_songs { + let song_id = song.id.ok_or(ServerFnError::ServerError::( + "Song id not found in database".to_string(), + ))?; - if let Some((stored_songdata, _)) = top_songs_map.get_mut(&song_id) { - // If the song is already in the map, update the artists - if let Some(artist) = artist { - stored_songdata.artists.push(artist); - } - } else { - let like_dislike = match (like, dislike) { - (Some(_), Some(_)) => Some((true, true)), - (Some(_), None) => Some((true, false)), - (None, Some(_)) => Some((false, true)), - _ => None, - }; + if let Some((stored_songdata, _)) = top_songs_map.get_mut(&song_id) { + // If the song is already in the map, update the artists + if let Some(artist) = artist { + stored_songdata.artists.push(artist); + } + } else { + let like_dislike = match (like, dislike) { + (Some(_), Some(_)) => Some((true, true)), + (Some(_), None) => Some((true, false)), + (None, Some(_)) => Some((false, true)), + _ => None, + }; - let image_path = song.image_path.unwrap_or( - album.as_ref().and_then(|album| album.image_path.clone()) - .unwrap_or("/assets/images/placeholders/MusicPlaceholder.svg".to_string())); + let image_path = song.image_path.unwrap_or( + album + .as_ref() + .and_then(|album| album.image_path.clone()) + .unwrap_or("/assets/images/placeholders/MusicPlaceholder.svg".to_string()), + ); - let songdata = frontend::Song { - id: song_id, - title: song.title, - artists: artist.map(|artist| vec![artist]).unwrap_or_default(), - album, - track: song.track, - duration: song.duration, - release_date: song.release_date, - song_path: song.storage_path, - image_path, - like_dislike, - added_date: song.added_date.unwrap(), - }; + let songdata = frontend::Song { + id: song_id, + title: song.title, + artists: artist.map(|artist| vec![artist]).unwrap_or_default(), + album, + track: song.track, + duration: song.duration, + release_date: song.release_date, + song_path: song.storage_path, + image_path, + like_dislike, + added_date: song.added_date.unwrap(), + }; - let plays = song_play_counts.get(&song_id) - .ok_or(ServerFnError::ServerError::("Song id not found in history counts".to_string()))?; + let plays = song_play_counts + .get(&song_id) + .ok_or(ServerFnError::ServerError::( + "Song id not found in history counts".to_string(), + ))?; - top_songs_map.insert(song_id, (songdata, *plays)); - } - } + top_songs_map.insert(song_id, (songdata, *plays)); + } + } let mut top_songs: Vec<(frontend::Song, i64)> = top_songs_map.into_values().collect(); top_songs.sort_by(|(_, plays1), (_, plays2)| plays2.cmp(plays1)); @@ -161,7 +196,10 @@ pub async fn top_songs_by_artist(artist_id: i32, limit: Option) -> Result) -> Result, ServerFnError> { +pub async fn albums_by_artist( + artist_id: i32, + limit: Option, +) -> Result, ServerFnError> { use crate::schema::*; let db = &mut get_db_conn(); @@ -177,18 +215,23 @@ pub async fn albums_by_artist(artist_id: i32, limit: Option) -> Result = HashMap::new(); let album_artists: Vec<(Album, Artist)> = albums::table .filter(albums::id.eq_any(album_ids)) - .inner_join(album_artists::table.inner_join(artists::table).on(albums::id.eq(album_artists::album_id))) + .inner_join( + album_artists::table + .inner_join(artists::table) + .on(albums::id.eq(album_artists::album_id)), + ) .select((albums::all_columns, artists::all_columns)) .load(db)?; for (album, artist) in album_artists { - let album_id = album.id - .ok_or(ServerFnError::ServerError::("Album id not found in database".to_string()))?; + let album_id = album.id.ok_or(ServerFnError::ServerError::( + "Album id not found in database".to_string(), + ))?; if let Some(stored_album) = albums_map.get_mut(&album_id) { stored_album.artists.push(artist); @@ -198,7 +241,9 @@ pub async fn albums_by_artist(artist_id: i32, limit: Option) -> Result Result<(), ServerFnError> { - // 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())); - } + // 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(), + )); + } - use crate::api::users::create_user; + use crate::api::users::create_user; - // Ensure the user has no id, and is not a self-proclaimed admin - let new_user = User { - id: None, - admin: false, - ..new_user - }; + // Ensure the user has no id, and is not a self-proclaimed admin + let new_user = User { + id: None, + admin: false, + ..new_user + }; - create_user(&new_user).await - .map_err(|e| ServerFnError::::ServerError(format!("Error creating user: {e}")))?; + create_user(&new_user).await.map_err(|e| { + ServerFnError::::ServerError(format!("Error creating user: {e}")) + })?; - let mut auth_session = extract::>().await - .map_err(|e| ServerFnError::::ServerError(format!("Error getting auth session: {e}")))?; + let mut auth_session = extract::>().await.map_err(|e| { + ServerFnError::::ServerError(format!("Error getting auth session: {e}")) + })?; - let credentials = UserCredentials { - username_or_email: new_user.username.clone(), - password: new_user.password.clone().unwrap() - }; + let credentials = UserCredentials { + username_or_email: new_user.username.clone(), + password: new_user.password.clone().unwrap(), + }; - 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}"))) - } - } + 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}" + ))), + } } /// Log a user in @@ -63,47 +66,53 @@ pub async fn signup(new_user: User) -> Result<(), ServerFnError> { /// Returns a Result with a boolean indicating if the login was successful #[server(endpoint = "login")] pub async fn login(credentials: UserCredentials) -> Result, ServerFnError> { - use crate::api::users::validate_user; + 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.map_err(|e| { + ServerFnError::::ServerError(format!("Error getting auth session: {e}")) + })?; - let user = validate_user(credentials).await - .map_err(|e| ServerFnError::::ServerError(format!("Error validating user: {e}")))?; + let user = validate_user(credentials).await.map_err(|e| { + ServerFnError::::ServerError(format!("Error validating user: {e}")) + })?; - if let Some(mut user) = user { - auth_session.login(&user).await - .map_err(|e| ServerFnError::::ServerError(format!("Error logging in user: {e}")))?; + if let Some(mut user) = user { + auth_session.login(&user).await.map_err(|e| { + ServerFnError::::ServerError(format!("Error logging in user: {e}")) + })?; - user.password = None; - Ok(Some(user)) - } else { - Ok(None) - } + user.password = None; + Ok(Some(user)) + } else { + Ok(None) + } } /// Log a user out /// Returns a Result with the error message if the user could not be logged out #[server(endpoint = "logout")] pub async fn logout() -> Result<(), ServerFnError> { - let mut auth_session = extract::>().await - .map_err(|e| ServerFnError::::ServerError(format!("Error getting auth session: {e}")))?; + let mut auth_session = extract::>().await.map_err(|e| { + ServerFnError::::ServerError(format!("Error getting auth session: {e}")) + })?; - auth_session.logout().await - .map_err(|e| ServerFnError::::ServerError(format!("Error getting auth session: {e}")))?; + auth_session.logout().await.map_err(|e| { + ServerFnError::::ServerError(format!("Error getting auth session: {e}")) + })?; - leptos_axum::redirect("/login"); - Ok(()) + leptos_axum::redirect("/login"); + Ok(()) } /// Check if a user is logged in /// Returns a Result with a boolean indicating if the user is logged in #[server(endpoint = "check_auth")] pub async fn check_auth() -> Result { - let auth_session = extract::>().await - .map_err(|e| ServerFnError::::ServerError(format!("Error getting auth session: {e}")))?; + let auth_session = extract::>().await.map_err(|e| { + ServerFnError::::ServerError(format!("Error getting auth session: {e}")) + })?; - Ok(auth_session.user.is_some()) + Ok(auth_session.user.is_some()) } /// Require that a user is logged in @@ -121,13 +130,15 @@ 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())) - } - }) + check_auth().await.and_then(|logged_in| { + if logged_in { + Ok(()) + } else { + Err(ServerFnError::::ServerError( + "Unauthorized".to_string(), + )) + } + }) } /// Get the current logged-in user @@ -147,33 +158,40 @@ 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}")))?; + let auth_session = extract::>().await.map_err(|e| { + ServerFnError::::ServerError(format!("Error getting auth session: {e}")) + })?; - auth_session.user.ok_or(ServerFnError::::ServerError("User not logged in".to_string())) + auth_session + .user + .ok_or(ServerFnError::::ServerError( + "User not logged in".to_string(), + )) } #[server(endpoint = "get_logged_in_user")] 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}")))?; + let auth_session = extract::>().await.map_err(|e| { + ServerFnError::::ServerError(format!("Error getting auth session: {e}")) + })?; - let user = auth_session.user.map(|mut user| { - user.password = None; - user - }); + let user = auth_session.user.map(|mut user| { + user.password = None; + user + }); - Ok(user) + Ok(user) } /// 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")] pub async fn check_admin() -> Result { - let auth_session = extract::>().await - .map_err(|e| ServerFnError::::ServerError(format!("Error getting auth session: {e}")))?; + let auth_session = extract::>().await.map_err(|e| { + ServerFnError::::ServerError(format!("Error getting auth session: {e}")) + })?; - Ok(auth_session.user.as_ref().map(|u| u.admin).unwrap_or(false)) + Ok(auth_session.user.as_ref().map(|u| u.admin).unwrap_or(false)) } /// Require that a user is logged in and an admin @@ -191,11 +209,13 @@ pub async fn check_admin() -> Result { /// ``` #[cfg(feature = "ssr")] pub async fn require_admin() -> Result<(), ServerFnError> { - check_admin().await.and_then(|is_admin| { - if is_admin { - Ok(()) - } else { - Err(ServerFnError::::ServerError("Unauthorized".to_string())) - } - }) + check_admin().await.and_then(|is_admin| { + if is_admin { + Ok(()) + } else { + Err(ServerFnError::::ServerError( + "Unauthorized".to_string(), + )) + } + }) } diff --git a/src/api/history.rs b/src/api/history.rs index b7943ef..1f3a735 100644 --- a/src/api/history.rs +++ b/src/api/history.rs @@ -1,44 +1,49 @@ -use chrono::NaiveDateTime; -use leptos::prelude::*; use crate::models::backend::HistoryEntry; use crate::models::backend::Song; +use chrono::NaiveDateTime; +use leptos::prelude::*; 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; - } + if #[cfg(feature = "ssr")] { + use leptos::server_fn::error::NoCustomError; + use crate::util::database::get_db_conn; + use crate::api::auth::get_user; + } } /// Get the history of the current user. #[server(endpoint = "history/get")] pub async fn get_history(limit: Option) -> Result, ServerFnError> { - let user = get_user().await?; - 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}")))?; - Ok(history) + let user = get_user().await?; + 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}")) + })?; + Ok(history) } /// Get the listen dates and songs of the current user. #[server(endpoint = "history/get_songs")] -pub async fn get_history_songs(limit: Option) -> Result, ServerFnError> { - let user = get_user().await?; - 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}")))?; - Ok(songs) +pub async fn get_history_songs( + limit: Option, +) -> Result, ServerFnError> { + let user = get_user().await?; + 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}")) + })?; + Ok(songs) } /// Add a song to the history of the current user. #[server(endpoint = "history/add")] pub async fn add_history(song_id: i32) -> Result<(), ServerFnError> { - let user = get_user().await?; - let db_con = &mut get_db_conn(); - user.add_history(song_id, db_con) - .map_err(|e| ServerFnError::::ServerError(format!("Error adding history: {e}")))?; - Ok(()) + let user = get_user().await?; + let db_con = &mut get_db_conn(); + user.add_history(song_id, db_con).map_err(|e| { + ServerFnError::::ServerError(format!("Error adding history: {e}")) + })?; + Ok(()) } diff --git a/src/api/mod.rs b/src/api/mod.rs index 5974c36..e0d7f22 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,10 +1,10 @@ -pub mod artists; +pub mod album; pub mod albums; +pub mod artists; +pub mod auth; pub mod history; pub mod profile; -pub mod songs; -pub mod album; pub mod search; +pub mod songs; pub mod upload; -pub mod auth; pub mod users; diff --git a/src/api/profile.rs b/src/api/profile.rs index e78ec05..d552085 100644 --- a/src/api/profile.rs +++ b/src/api/profile.rs @@ -7,56 +7,68 @@ use crate::models::frontend; use chrono::NaiveDateTime; cfg_if! { - if #[cfg(feature = "ssr")] { - use crate::api::auth::get_user; - use server_fn::error::NoCustomError; + 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::*; - use diesel::dsl::count; - use crate::models::backend::{Album, Artist, Song, HistoryEntry}; - use crate::schema::*; + use crate::util::database::get_db_conn; + use diesel::prelude::*; + use diesel::dsl::count; + use crate::models::backend::{Album, Artist, Song, HistoryEntry}; + use crate::schema::*; - use std::collections::HashMap; - } + use std::collections::HashMap; + } } /// 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> { - // 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(); + // 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()))?; + 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()))?; - if field.name() != Some("picture") { - return Err(ServerFnError::ServerError("Field name is not 'picture'".to_string())); - } + if field.name() != Some("picture") { + return Err(ServerFnError::ServerError( + "Field name is not 'picture'".to_string(), + )); + } - // Get user id from session - let user = get_user().await - .map_err(|e| ServerFnError::::ServerError(format!("Error getting user: {e}")))?; + // Get user id from session + let user = get_user().await.map_err(|e| { + ServerFnError::::ServerError(format!("Error getting user: {e}")) + })?; - let user_id = user.id.ok_or_else(|| ServerFnError::::ServerError("User has no id".to_string()))?; + let user_id = user + .id + .ok_or_else(|| ServerFnError::::ServerError("User has no id".to_string()))?; - // Read the image, and convert it to webp - use image_convert::{to_webp, WEBPConfig, ImageResource}; + // 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| { + ServerFnError::::ServerError(format!("Error getting field bytes: {e}")) + })?; - 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 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 profile_picture_path = format!("assets/images/profile/{user_id}.webp"); - 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}")))?; + let profile_picture_path = format!("assets/images/profile/{user_id}.webp"); + 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}")) + })?; - Ok(()) + Ok(()) } /// Get a user's recent songs listened to @@ -65,92 +77,121 @@ pub async fn upload_picture(data: MultipartData) -> Result<(), ServerFnError> { /// Returns a list of tuples with the date the song was listened to /// and the song data, sorted by date (most recent first). #[server(endpoint = "/profile/recent_songs")] -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.unwrap(); +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 + .unwrap(); - let mut db_con = get_db_conn(); + let mut db_con = get_db_conn(); - // Create an alias for the table so it can be referenced twice in the query - let history2 = diesel::alias!(song_history as history2); + // Create an alias for the table so it can be referenced twice in the query + let history2 = diesel::alias!(song_history as history2); - // Get the ids of the most recent songs listened to - let history_ids = history2 - .filter(history2.fields(song_history::user_id).eq(for_user_id)) - .order(history2.fields(song_history::date).desc()) - .select(history2.fields(song_history::id)); + // Get the ids of the most recent songs listened to + let history_ids = history2 + .filter(history2.fields(song_history::user_id).eq(for_user_id)) + .order(history2.fields(song_history::date).desc()) + .select(history2.fields(song_history::id)); - let history_ids = if let Some(limit) = limit { - history_ids.limit(limit).into_boxed() - } else { - history_ids.into_boxed() - }; + let history_ids = if let Some(limit) = limit { + history_ids.limit(limit).into_boxed() + } else { + history_ids.into_boxed() + }; - // Take the history ids and get the song data for them - let history: Vec<(HistoryEntry, Song, Option, Option, Option<(i32, i32)>, Option<(i32, i32)>)> - = song_history::table - .filter(song_history::id.eq_any(history_ids)) - .inner_join(songs::table) - .left_join(albums::table.on(songs::album_id.eq(albums::id.nullable()))) - .left_join(song_artists::table.inner_join(artists::table).on(songs::id.eq(song_artists::song_id))) - .left_join(song_likes::table.on(songs::id.eq(song_likes::song_id).and(song_likes::user_id.eq(viewing_user_id)))) - .left_join(song_dislikes::table.on( - songs::id.eq(song_dislikes::song_id).and(song_dislikes::user_id.eq(viewing_user_id)))) - .select(( - song_history::all_columns, - songs::all_columns, - albums::all_columns.nullable(), - artists::all_columns.nullable(), - song_likes::all_columns.nullable(), - song_dislikes::all_columns.nullable(), - )) - .load(&mut db_con)?; + // Take the history ids and get the song data for them + let history: Vec<( + HistoryEntry, + Song, + Option, + Option, + Option<(i32, i32)>, + Option<(i32, i32)>, + )> = song_history::table + .filter(song_history::id.eq_any(history_ids)) + .inner_join(songs::table) + .left_join(albums::table.on(songs::album_id.eq(albums::id.nullable()))) + .left_join( + song_artists::table + .inner_join(artists::table) + .on(songs::id.eq(song_artists::song_id)), + ) + .left_join( + song_likes::table.on(songs::id + .eq(song_likes::song_id) + .and(song_likes::user_id.eq(viewing_user_id))), + ) + .left_join( + song_dislikes::table.on(songs::id + .eq(song_dislikes::song_id) + .and(song_dislikes::user_id.eq(viewing_user_id))), + ) + .select(( + song_history::all_columns, + songs::all_columns, + albums::all_columns.nullable(), + artists::all_columns.nullable(), + song_likes::all_columns.nullable(), + song_dislikes::all_columns.nullable(), + )) + .load(&mut db_con)?; - // Process the history data into a map of song ids to song data - let mut history_songs: HashMap = HashMap::new(); + // Process the history data into a map of song ids to song data + let mut history_songs: HashMap = HashMap::new(); - for (history, song, album, artist, like, dislike) in history { - let song_id = history.song_id; + for (history, song, album, artist, like, dislike) in history { + let song_id = history.song_id; - if let Some((_, stored_songdata)) = history_songs.get_mut(&song_id) { - // If the song is already in the map, update the artists - if let Some(artist) = artist { - stored_songdata.artists.push(artist); - } - } else { - let like_dislike = match (like, dislike) { - (Some(_), Some(_)) => Some((true, true)), - (Some(_), None) => Some((true, false)), - (None, Some(_)) => Some((false, true)), - _ => None, - }; + if let Some((_, stored_songdata)) = history_songs.get_mut(&song_id) { + // If the song is already in the map, update the artists + if let Some(artist) = artist { + stored_songdata.artists.push(artist); + } + } else { + let like_dislike = match (like, dislike) { + (Some(_), Some(_)) => Some((true, true)), + (Some(_), None) => Some((true, false)), + (None, Some(_)) => Some((false, true)), + _ => None, + }; - let image_path = song.image_path.unwrap_or( - album.as_ref().and_then(|album| album.image_path.clone()) - .unwrap_or("/assets/images/placeholders/MusicPlaceholder.svg".to_string())); + let image_path = song.image_path.unwrap_or( + album + .as_ref() + .and_then(|album| album.image_path.clone()) + .unwrap_or("/assets/images/placeholders/MusicPlaceholder.svg".to_string()), + ); - let songdata = frontend::Song { - id: song_id, - title: song.title, - artists: artist.map(|artist| vec![artist]).unwrap_or_default(), - album, - track: song.track, - duration: song.duration, - release_date: song.release_date, - song_path: song.storage_path, - image_path, - like_dislike, - added_date: song.added_date.unwrap(), - }; + let songdata = frontend::Song { + id: song_id, + title: song.title, + artists: artist.map(|artist| vec![artist]).unwrap_or_default(), + album, + track: song.track, + duration: song.duration, + release_date: song.release_date, + song_path: song.storage_path, + image_path, + like_dislike, + added_date: song.added_date.unwrap(), + }; - history_songs.insert(song_id, (history.date, songdata)); - } - } + history_songs.insert(song_id, (history.date, songdata)); + } + } - // Sort the songs by date - let mut history_songs: Vec<(NaiveDateTime, frontend::Song)> = history_songs.into_values().collect(); - history_songs.sort_by(|a, b| b.0.cmp(&a.0)); - Ok(history_songs) + // Sort the songs by date + let mut history_songs: Vec<(NaiveDateTime, frontend::Song)> = + history_songs.into_values().collect(); + history_songs.sort_by(|a, b| b.0.cmp(&a.0)); + Ok(history_songs) } /// Get a user's top songs by play count from a date range @@ -158,103 +199,135 @@ pub async fn recent_songs(for_user_id: i32, limit: Option) -> Result) - -> Result, ServerFnError> -{ let viewing_user_id = get_user().await - .map_err(|e| ServerFnError::::ServerError(format!("Error getting user: {e}")))?.id.unwrap(); +pub async fn top_songs( + for_user_id: i32, + 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 + .unwrap(); - let mut db_con = get_db_conn(); + let mut db_con = get_db_conn(); - // Get the play count and ids of the songs listened to in the date range - let history_counts: Vec<(i32, i64)> = - if let Some(limit) = limit { - 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))) - .order(count(song_history::song_id).desc()) - .limit(limit) - .load(&mut db_con)? - } 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)? - }; + // Get the play count and ids of the songs listened to in the date range + let history_counts: Vec<(i32, i64)> = if let Some(limit) = limit { + 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))) + .order(count(song_history::song_id).desc()) + .limit(limit) + .load(&mut db_con)? + } 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)? + }; - let history_counts: HashMap = history_counts.into_iter().collect(); - let history_song_ids = history_counts.keys().copied().collect::>(); + let history_counts: HashMap = history_counts.into_iter().collect(); + let history_song_ids = history_counts.keys().copied().collect::>(); - // Get the song data for the songs listened to in the date range - let history_songs: Vec<(Song, Option, Option, Option<(i32, i32)>, Option<(i32, i32)>)> - = songs::table - .filter(songs::id.eq_any(history_song_ids)) - .left_join(albums::table.on(songs::album_id.eq(albums::id.nullable()))) - .left_join(song_artists::table.inner_join(artists::table).on(songs::id.eq(song_artists::song_id))) - .left_join(song_likes::table.on(songs::id.eq(song_likes::song_id).and(song_likes::user_id.eq(viewing_user_id)))) - .left_join(song_dislikes::table.on( - songs::id.eq(song_dislikes::song_id).and(song_dislikes::user_id.eq(viewing_user_id)))) - .select(( - songs::all_columns, - albums::all_columns.nullable(), - artists::all_columns.nullable(), - song_likes::all_columns.nullable(), - song_dislikes::all_columns.nullable(), - )) - .load(&mut db_con)?; + // Get the song data for the songs listened to in the date range + let history_songs: Vec<( + Song, + Option, + Option, + Option<(i32, i32)>, + Option<(i32, i32)>, + )> = songs::table + .filter(songs::id.eq_any(history_song_ids)) + .left_join(albums::table.on(songs::album_id.eq(albums::id.nullable()))) + .left_join( + song_artists::table + .inner_join(artists::table) + .on(songs::id.eq(song_artists::song_id)), + ) + .left_join( + song_likes::table.on(songs::id + .eq(song_likes::song_id) + .and(song_likes::user_id.eq(viewing_user_id))), + ) + .left_join( + song_dislikes::table.on(songs::id + .eq(song_dislikes::song_id) + .and(song_dislikes::user_id.eq(viewing_user_id))), + ) + .select(( + songs::all_columns, + albums::all_columns.nullable(), + artists::all_columns.nullable(), + song_likes::all_columns.nullable(), + song_dislikes::all_columns.nullable(), + )) + .load(&mut db_con)?; - // Process the history data into a map of song ids to song data - let mut history_songs_map: HashMap = HashMap::with_capacity(history_counts.len()); + // Process the history data into a map of song ids to song data + let mut history_songs_map: HashMap = + HashMap::with_capacity(history_counts.len()); - for (song, album, artist, like, dislike) in history_songs { - let song_id = song.id - .ok_or(ServerFnError::ServerError::("Song id not found in database".to_string()))?; + for (song, album, artist, like, dislike) in history_songs { + let song_id = song.id.ok_or(ServerFnError::ServerError::( + "Song id not found in database".to_string(), + ))?; - if let Some((_, stored_songdata)) = history_songs_map.get_mut(&song_id) { - // If the song is already in the map, update the artists - if let Some(artist) = artist { - stored_songdata.artists.push(artist); - } - } else { - let like_dislike = match (like, dislike) { - (Some(_), Some(_)) => Some((true, true)), - (Some(_), None) => Some((true, false)), - (None, Some(_)) => Some((false, true)), - _ => None, - }; + if let Some((_, stored_songdata)) = history_songs_map.get_mut(&song_id) { + // If the song is already in the map, update the artists + if let Some(artist) = artist { + stored_songdata.artists.push(artist); + } + } else { + let like_dislike = match (like, dislike) { + (Some(_), Some(_)) => Some((true, true)), + (Some(_), None) => Some((true, false)), + (None, Some(_)) => Some((false, true)), + _ => None, + }; - let image_path = song.image_path.unwrap_or( - album.as_ref().and_then(|album| album.image_path.clone()) - .unwrap_or("/assets/images/placeholders/MusicPlaceholder.svg".to_string())); + let image_path = song.image_path.unwrap_or( + album + .as_ref() + .and_then(|album| album.image_path.clone()) + .unwrap_or("/assets/images/placeholders/MusicPlaceholder.svg".to_string()), + ); - let songdata = frontend::Song { - id: song_id, - title: song.title, - artists: artist.map(|artist| vec![artist]).unwrap_or_default(), - album, - track: song.track, - duration: song.duration, - release_date: song.release_date, - song_path: song.storage_path, - image_path, - like_dislike, - added_date: song.added_date.unwrap(), - }; + let songdata = frontend::Song { + id: song_id, + title: song.title, + artists: artist.map(|artist| vec![artist]).unwrap_or_default(), + album, + track: song.track, + duration: song.duration, + release_date: song.release_date, + song_path: song.storage_path, + image_path, + like_dislike, + added_date: song.added_date.unwrap(), + }; - let plays = history_counts.get(&song_id) - .ok_or(ServerFnError::ServerError::("Song id not found in history counts".to_string()))?; + let plays = history_counts + .get(&song_id) + .ok_or(ServerFnError::ServerError::( + "Song id not found in history counts".to_string(), + ))?; - history_songs_map.insert(song_id, (*plays, songdata)); - } - } + history_songs_map.insert(song_id, (*plays, songdata)); + } + } - // Sort the songs by play count - let mut history_songs: Vec<(i64, frontend::Song)> = history_songs_map.into_values().collect(); - history_songs.sort_by(|a, b| b.0.cmp(&a.0)); - Ok(history_songs) + // Sort the songs by play count + let mut history_songs: Vec<(i64, frontend::Song)> = history_songs_map.into_values().collect(); + history_songs.sort_by(|a, b| b.0.cmp(&a.0)); + Ok(history_songs) } /// Get a user's top artists by play count from a date range @@ -262,42 +335,50 @@ pub async fn top_songs(for_user_id: i32, start_date: NaiveDateTime, end_date: Na /// If not provided, all artists listened to in the date range are returned. /// Returns a list of tuples with the play count and the artist data, sorted by play count (most played first). #[server(endpoint = "/profile/top_artists")] -pub async fn top_artists(for_user_id: i32, start_date: NaiveDateTime, end_date: NaiveDateTime, limit: Option) - -> Result, ServerFnError> -{ - let mut db_con = get_db_conn(); +pub async fn top_artists( + for_user_id: i32, + start_date: NaiveDateTime, + end_date: NaiveDateTime, + limit: Option, +) -> Result, ServerFnError> { + let mut db_con = get_db_conn(); - let artist_counts: Vec<(i64, Artist)> = - if let Some(limit) = limit { - song_history::table - .filter(song_history::date.between(start_date, end_date)) - .filter(song_history::user_id.eq(for_user_id)) - .inner_join(song_artists::table.on(song_history::song_id.eq(song_artists::song_id))) - .inner_join(artists::table.on(song_artists::artist_id.eq(artists::id))) - .group_by(artists::id) - .select((count(artists::id), artists::all_columns)) - .order(count(artists::id).desc()) - .limit(limit) - .load(&mut db_con)? - } else { - song_history::table - .filter(song_history::date.between(start_date, end_date)) - .filter(song_history::user_id.eq(for_user_id)) - .inner_join(song_artists::table.on(song_history::song_id.eq(song_artists::song_id))) - .inner_join(artists::table.on(song_artists::artist_id.eq(artists::id))) - .group_by(artists::id) - .select((count(artists::id), artists::all_columns)) - .order(count(artists::id).desc()) - .load(&mut db_con)? - }; + let artist_counts: Vec<(i64, Artist)> = if let Some(limit) = limit { + song_history::table + .filter(song_history::date.between(start_date, end_date)) + .filter(song_history::user_id.eq(for_user_id)) + .inner_join(song_artists::table.on(song_history::song_id.eq(song_artists::song_id))) + .inner_join(artists::table.on(song_artists::artist_id.eq(artists::id))) + .group_by(artists::id) + .select((count(artists::id), artists::all_columns)) + .order(count(artists::id).desc()) + .limit(limit) + .load(&mut db_con)? + } else { + song_history::table + .filter(song_history::date.between(start_date, end_date)) + .filter(song_history::user_id.eq(for_user_id)) + .inner_join(song_artists::table.on(song_history::song_id.eq(song_artists::song_id))) + .inner_join(artists::table.on(song_artists::artist_id.eq(artists::id))) + .group_by(artists::id) + .select((count(artists::id), artists::all_columns)) + .order(count(artists::id).desc()) + .load(&mut db_con)? + }; - let artist_data: Vec<(i64, frontend::Artist)> = artist_counts.into_iter().map(|(plays, artist)| { - (plays, frontend::Artist { - id: artist.id.unwrap(), - name: artist.name, - image_path: format!("/assets/images/artist/{}.webp", artist.id.unwrap()), - }) - }).collect(); + let artist_data: Vec<(i64, frontend::Artist)> = artist_counts + .into_iter() + .map(|(plays, artist)| { + ( + plays, + frontend::Artist { + id: artist.id.unwrap(), + name: artist.name, + image_path: format!("/assets/images/artist/{}.webp", artist.id.unwrap()), + }, + ) + }) + .collect(); - Ok(artist_data) + Ok(artist_data) } diff --git a/src/api/search.rs b/src/api/search.rs index cd8096e..44b25d2 100644 --- a/src/api/search.rs +++ b/src/api/search.rs @@ -1,109 +1,112 @@ +use crate::models::backend::{Album, Artist, Song}; use leptos::prelude::*; -use crate::models::backend::{Artist, Album, Song}; use cfg_if::cfg_if; cfg_if! { if #[cfg(feature = "ssr")] { - use diesel::sql_types::*; - use diesel::*; - use diesel::pg::Pg; - use diesel::expression::AsExpression; + use diesel::sql_types::*; + use diesel::*; + use diesel::pg::Pg; + use diesel::expression::AsExpression; - use crate::util::database::get_db_conn; + use crate::util::database::get_db_conn; - // Define pg_trgm operators - // Functions do not use indices for queries, so we need to use operators - diesel::infix_operator!(Similarity, " % ", backend: Pg); - diesel::infix_operator!(Distance, " <-> ", Float, backend: Pg); + // Define pg_trgm operators + // Functions do not use indices for queries, so we need to use operators + diesel::infix_operator!(Similarity, " % ", backend: Pg); + diesel::infix_operator!(Distance, " <-> ", Float, backend: Pg); - // Create functions to make use of the operators in queries - fn trgm_similar, U: AsExpression>(left: T, right: U) - -> Similarity { - Similarity::new(left.as_expression(), right.as_expression()) - } + // Create functions to make use of the operators in queries + fn trgm_similar, U: AsExpression>(left: T, right: U) + -> Similarity { + Similarity::new(left.as_expression(), right.as_expression()) + } - fn trgm_distance, U: AsExpression>(left: T, right: U) - -> Distance { - Distance::new(left.as_expression(), right.as_expression()) - } + fn trgm_distance, U: AsExpression>(left: T, right: U) + -> Distance { + Distance::new(left.as_expression(), right.as_expression()) + } } } /// Search for albums by title -/// +/// /// # Arguments /// `query` - The search query. This will be used to perform a fuzzy search on the album titles /// `limit` - The maximum number of results to return -/// +/// /// # Returns /// A Result containing a vector of albums if the search was successful, or an error if the search failed #[server(endpoint = "search_albums")] pub async fn search_albums(query: String, limit: i64) -> Result, ServerFnError> { - use crate::schema::albums::dsl::*; + use crate::schema::albums::dsl::*; - Ok(albums - .filter(trgm_similar(title, query.clone())) - .order_by(trgm_distance(title, query)) - .limit(limit) - .load(&mut get_db_conn())?) + Ok(albums + .filter(trgm_similar(title, query.clone())) + .order_by(trgm_distance(title, query)) + .limit(limit) + .load(&mut get_db_conn())?) } /// Search for artists by name -/// +/// /// # Arguments /// `query` - The search query. This will be used to perform a fuzzy search on the artist names /// `limit` - The maximum number of results to return -/// +/// /// # Returns /// A Result containing a vector of artists if the search was successful, or an error if the search failed #[server(endpoint = "search_artists")] pub async fn search_artists(query: String, limit: i64) -> Result, ServerFnError> { - use crate::schema::artists::dsl::*; + use crate::schema::artists::dsl::*; - Ok(artists - .filter(trgm_similar(name, query.clone())) - .order_by(trgm_distance(name, query)) - .limit(limit) - .load(&mut get_db_conn())?) + Ok(artists + .filter(trgm_similar(name, query.clone())) + .order_by(trgm_distance(name, query)) + .limit(limit) + .load(&mut get_db_conn())?) } /// Search for songs by title -/// +/// /// # Arguments /// `query` - The search query. This will be used to perform a fuzzy search on the song titles /// `limit` - The maximum number of results to return -/// +/// /// # Returns /// A Result containing a vector of songs if the search was successful, or an error if the search failed #[server(endpoint = "search_songs")] pub async fn search_songs(query: String, limit: i64) -> Result, ServerFnError> { - use crate::schema::songs::dsl::*; + use crate::schema::songs::dsl::*; - Ok(songs - .filter(trgm_similar(title, query.clone())) - .order_by(trgm_distance(title, query)) - .limit(limit) - .load(&mut get_db_conn())?) + Ok(songs + .filter(trgm_similar(title, query.clone())) + .order_by(trgm_distance(title, query)) + .limit(limit) + .load(&mut get_db_conn())?) } /// Search for songs, albums, and artists by title or name -/// +/// /// # Arguments /// `query` - The search query. This will be used to perform a fuzzy search on the /// song titles, album titles, and artist names /// `limit` - The maximum number of results to return for each type -/// +/// /// # Returns /// A Result containing a tuple of vectors of albums, artists, and songs if the search was successful, #[server(endpoint = "search")] -pub async fn search(query: String, limit: i64) -> Result<(Vec, Vec, Vec), ServerFnError> { - let albums = search_albums(query.clone(), limit); - let artists = search_artists(query.clone(), limit); - let songs = search_songs(query.clone(), limit); +pub async fn search( + query: String, + limit: i64, +) -> Result<(Vec, Vec, Vec), ServerFnError> { + let albums = search_albums(query.clone(), limit); + let artists = search_artists(query.clone(), limit); + let songs = search_songs(query.clone(), limit); - use tokio::join; + use tokio::join; - let (albums, artists, songs) = join!(albums, artists, songs); - Ok((albums?, artists?, songs?)) + let (albums, artists, songs) = join!(albums, artists, songs); + Ok((albums?, artists?, songs?)) } diff --git a/src/api/songs.rs b/src/api/songs.rs index baf1c2b..e8ef7c4 100644 --- a/src/api/songs.rs +++ b/src/api/songs.rs @@ -5,145 +5,190 @@ use cfg_if::cfg_if; use crate::models::frontend; 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}; - use diesel::prelude::*; - } + 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}; + use diesel::prelude::*; + } } /// Like or unlike a song #[server(endpoint = "songs/set_like")] 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}")))?; - - let db_con = &mut get_db_conn(); + let user = get_user().await.map_err(|e| { + ServerFnError::::ServerError(format!("Error getting user: {e}")) + })?; - user.set_like_song(song_id, like, db_con).await.map_err(|e| ServerFnError:::: - ServerError(format!("Error liking song: {e}"))) + 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}"))) } /// Dislike or remove dislike from a song #[server(endpoint = "songs/set_dislike")] 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}")))?; - - let db_con = &mut get_db_conn(); + let user = get_user().await.map_err(|e| { + ServerFnError::::ServerError(format!("Error getting user: {e}")) + })?; - user.set_dislike_song(song_id, dislike, db_con).await.map_err(|e| ServerFnError:::: - ServerError(format!("Error disliking song: {e}"))) + 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}")) + }) } /// Get the like and dislike status of a song #[server(endpoint = "songs/get_like_dislike")] 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}")))?; + let user = get_user().await.map_err(|e| { + ServerFnError::::ServerError(format!("Error getting user: {e}")) + })?; - let db_con = &mut get_db_conn(); + 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 + // 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.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}")) + })?; - Ok((like, dislike)) + Ok((like, dislike)) } #[server(endpoint = "songs/get")] pub async fn get_song_by_id(song_id: i32) -> Result, ServerFnError> { - use crate::schema::*; + use crate::schema::*; - let user_id: i32 = get_user().await.map_err(|e| ServerFnError:::: - ServerError(format!("Error getting user: {e}")))?.id.unwrap(); + let user_id: i32 = get_user() + .await + .map_err(|e| { + ServerFnError::::ServerError(format!("Error getting user: {e}")) + })? + .id + .unwrap(); - let db_con = &mut get_db_conn(); + let db_con = &mut get_db_conn(); - let song_parts: Vec<(Song, Option, Option, Option<(i32, i32)>, Option<(i32, i32)>)> - = songs::table - .find(song_id) - .left_join(albums::table.on(songs::album_id.eq(albums::id.nullable()))) - .left_join(song_artists::table.inner_join(artists::table).on(songs::id.eq(song_artists::song_id))) - .left_join(song_likes::table.on(songs::id.eq(song_likes::song_id).and(song_likes::user_id.eq(user_id)))) - .left_join(song_dislikes::table.on( - songs::id.eq(song_dislikes::song_id).and(song_dislikes::user_id.eq(user_id)))) - .select(( - songs::all_columns, - albums::all_columns.nullable(), - artists::all_columns.nullable(), - song_likes::all_columns.nullable(), - song_dislikes::all_columns.nullable(), - )) - .load(db_con)?; + let song_parts: Vec<( + Song, + Option, + Option, + Option<(i32, i32)>, + Option<(i32, i32)>, + )> = songs::table + .find(song_id) + .left_join(albums::table.on(songs::album_id.eq(albums::id.nullable()))) + .left_join( + song_artists::table + .inner_join(artists::table) + .on(songs::id.eq(song_artists::song_id)), + ) + .left_join( + song_likes::table.on(songs::id + .eq(song_likes::song_id) + .and(song_likes::user_id.eq(user_id))), + ) + .left_join( + song_dislikes::table.on(songs::id + .eq(song_dislikes::song_id) + .and(song_dislikes::user_id.eq(user_id))), + ) + .select(( + songs::all_columns, + albums::all_columns.nullable(), + artists::all_columns.nullable(), + song_likes::all_columns.nullable(), + song_dislikes::all_columns.nullable(), + )) + .load(db_con)?; - let song = song_parts.first().cloned(); - let artists = song_parts.into_iter().filter_map(|(_, _, artist, _, _)| artist).collect::>(); + let song = song_parts.first().cloned(); + let artists = song_parts + .into_iter() + .filter_map(|(_, _, artist, _, _)| artist) + .collect::>(); - match song { - Some((song, album, _artist, like, dislike)) => { - // Use song image path, or fall back to album image path, or fall back to placeholder - let image_path = song.image_path.clone().unwrap_or_else(|| { - album.as_ref().and_then(|album| album.image_path.clone()).unwrap_or( - "/assets/images/placeholders/MusicPlaceholder.svg".to_string() - ) - }); + match song { + Some((song, album, _artist, like, dislike)) => { + // Use song image path, or fall back to album image path, or fall back to placeholder + let image_path = song.image_path.clone().unwrap_or_else(|| { + album + .as_ref() + .and_then(|album| album.image_path.clone()) + .unwrap_or("/assets/images/placeholders/MusicPlaceholder.svg".to_string()) + }); - Ok(Some(frontend::Song { - id: song.id.unwrap(), - title: song.title.clone(), - artists, - album: album.clone(), - track: song.track, - duration: song.duration, - release_date: song.release_date, - song_path: song.storage_path.clone(), - image_path, - like_dislike: Some((like.is_some(), dislike.is_some())), - added_date: song.added_date.unwrap(), - })) - }, - None => Ok(None) - } + Ok(Some(frontend::Song { + id: song.id.unwrap(), + title: song.title.clone(), + artists, + album: album.clone(), + track: song.track, + duration: song.duration, + release_date: song.release_date, + song_path: song.storage_path.clone(), + image_path, + like_dislike: Some((like.is_some(), dislike.is_some())), + added_date: song.added_date.unwrap(), + })) + } + None => Ok(None), + } } #[server(endpoint = "songs/plays")] pub async fn get_song_plays(song_id: i32) -> Result { - use crate::schema::*; + use crate::schema::*; - let db_con = &mut get_db_conn(); + let db_con = &mut get_db_conn(); - let plays = song_history::table - .filter(song_history::song_id.eq(song_id)) - .count() - .get_result::(db_con) - .map_err(|e| ServerFnError:::: - ServerError(format!("Error getting song plays: {e}")))?; + let plays = song_history::table + .filter(song_history::song_id.eq(song_id)) + .count() + .get_result::(db_con) + .map_err(|e| { + ServerFnError::::ServerError(format!("Error getting song plays: {e}")) + })?; - Ok(plays) + Ok(plays) } #[server(endpoint = "songs/my-plays")] pub async fn get_my_song_plays(song_id: i32) -> Result { - use crate::schema::*; + use crate::schema::*; - let user_id: i32 = get_user().await.map_err(|e| ServerFnError:::: - ServerError(format!("Error getting user: {e}")))?.id.unwrap(); + let user_id: i32 = get_user() + .await + .map_err(|e| { + ServerFnError::::ServerError(format!("Error getting user: {e}")) + })? + .id + .unwrap(); - let db_con = &mut get_db_conn(); + let db_con = &mut get_db_conn(); - let plays = song_history::table - .filter(song_history::song_id.eq(song_id).and(song_history::user_id.eq(user_id))) - .count() - .get_result::(db_con) - .map_err(|e| ServerFnError:::: - ServerError(format!("Error getting song plays: {e}")))?; + let plays = song_history::table + .filter( + song_history::song_id + .eq(song_id) + .and(song_history::user_id.eq(user_id)), + ) + .count() + .get_result::(db_con) + .map_err(|e| { + ServerFnError::::ServerError(format!("Error getting song plays: {e}")) + })?; - Ok(plays) + Ok(plays) } diff --git a/src/api/upload.rs b/src/api/upload.rs index 705cb49..303bd33 100644 --- a/src/api/upload.rs +++ b/src/api/upload.rs @@ -4,25 +4,27 @@ use server_fn::codec::{MultipartData, MultipartFormData}; use cfg_if::cfg_if; cfg_if! { - if #[cfg(feature = "ssr")] { - use multer::Field; - use crate::util::database::get_db_conn; - use diesel::prelude::*; - use log::*; - use server_fn::error::NoCustomError; - use chrono::NaiveDate; - } + if #[cfg(feature = "ssr")] { + use multer::Field; + use crate::util::database::get_db_conn; + use diesel::prelude::*; + use log::*; + use server_fn::error::NoCustomError; + use chrono::NaiveDate; + } } /// Extract the text from a multipart field #[cfg(feature = "ssr")] 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}")))?, - }; + let field = match field.text().await { + Ok(field) => field, + Err(e) => Err(ServerFnError::::ServerError(format!( + "Error reading field: {e}" + )))?, + }; - Ok(field) + Ok(field) } /// Validate the artist ids in a multipart field @@ -30,265 +32,324 @@ async fn extract_field(field: Field<'static>) -> Result { #[cfg(feature = "ssr")] async fn validate_artist_ids(artist_ids: Field<'static>) -> Result, ServerFnError> { use crate::models::backend::Artist; - use diesel::result::Error::NotFound; + use diesel::result::Error::NotFound; - // Extract the artist id from the field - match artist_ids.text().await { - Ok(artist_ids) => { - let artist_ids = artist_ids.trim_end_matches(',').split(','); + // Extract the artist id from the field + match artist_ids.text().await { + Ok(artist_ids) => { + let artist_ids = artist_ids.trim_end_matches(',').split(','); - artist_ids.filter(|artist_id| !artist_id.is_empty()).map(|artist_id| { - // Parse the artist id as an integer - if let Ok(artist_id) = artist_id.parse::() { - // Check if the artist exists - let db_con = &mut get_db_conn(); - let artist = crate::schema::artists::dsl::artists.find(artist_id).first::(db_con); + artist_ids + .filter(|artist_id| !artist_id.is_empty()) + .map(|artist_id| { + // Parse the artist id as an integer + if let Ok(artist_id) = artist_id.parse::() { + // Check if the artist exists + let db_con = &mut get_db_conn(); + let artist = crate::schema::artists::dsl::artists + .find(artist_id) + .first::(db_con); - 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}"))), - } - } else { - Err(ServerFnError::::ServerError("Error parsing artist id".to_string())) - } - }).collect() - }, + 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}" + ))), + } + } else { + Err(ServerFnError::::ServerError( + "Error parsing artist id".to_string(), + )) + } + }) + .collect() + } - Err(e) => Err(ServerFnError::::ServerError(format!("Error reading artist id: {e}"))), - } + Err(e) => Err(ServerFnError::::ServerError(format!( + "Error reading artist id: {e}" + ))), + } } /// 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> { - use crate::models::backend::Album; - use diesel::result::Error::NotFound; + use crate::models::backend::Album; + use diesel::result::Error::NotFound; - // Extract the album id from the field - match album_id.text().await { - Ok(album_id) => { - if album_id.is_empty() { - return Ok(None); - } + // Extract the album id from the field + match album_id.text().await { + Ok(album_id) => { + if album_id.is_empty() { + return Ok(None); + } - // Parse the album id as an integer - if let Ok(album_id) = album_id.parse::() { - // Check if the album exists - let db_con = &mut get_db_conn(); - let album = crate::schema::albums::dsl::albums.find(album_id).first::(db_con); + // Parse the album id as an integer + if let Ok(album_id) = album_id.parse::() { + // Check if the album exists + let db_con = &mut get_db_conn(); + let album = crate::schema::albums::dsl::albums + .find(album_id) + .first::(db_con); - 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}"))), - } - } else { - Err(ServerFnError::::ServerError("Error parsing album id".to_string())) - } - }, - Err(e) => Err(ServerFnError::::ServerError(format!("Error reading album id: {e}"))), - } + 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}" + ))), + } + } else { + Err(ServerFnError::::ServerError( + "Error parsing album id".to_string(), + )) + } + } + Err(e) => Err(ServerFnError::::ServerError(format!( + "Error reading album id: {e}" + ))), + } } /// 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> { - match track_number.text().await { - Ok(track_number) => { - if track_number.is_empty() { - return Ok(None); - } + match track_number.text().await { + Ok(track_number) => { + if track_number.is_empty() { + return Ok(None); + } - if let Ok(track_number) = track_number.parse::() { - if track_number < 0 { - Err(ServerFnError:::: - ServerError("Track number must be positive or 0".to_string())) - } else { - Ok(Some(track_number)) - } - } else { - Err(ServerFnError::::ServerError("Error parsing track number".to_string())) - } - }, - Err(e) => Err(ServerFnError::::ServerError(format!("Error reading track number: {e}")))?, - } + if let Ok(track_number) = track_number.parse::() { + if track_number < 0 { + Err(ServerFnError::::ServerError( + "Track number must be positive or 0".to_string(), + )) + } else { + Ok(Some(track_number)) + } + } else { + Err(ServerFnError::::ServerError( + "Error parsing track number".to_string(), + )) + } + } + Err(e) => Err(ServerFnError::::ServerError(format!( + "Error reading track number: {e}" + )))?, + } } /// 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> { - match release_date.text().await { - Ok(release_date) => { - if release_date.trim().is_empty() { - return Ok(None); - } +async fn validate_release_date( + release_date: Field<'static>, +) -> Result, ServerFnError> { + match release_date.text().await { + Ok(release_date) => { + if release_date.trim().is_empty() { + return Ok(None); + } - let release_date = NaiveDate::parse_from_str(release_date.trim(), "%Y-%m-%d"); + let release_date = NaiveDate::parse_from_str(release_date.trim(), "%Y-%m-%d"); - match release_date { - Ok(release_date) => Ok(Some(release_date)), - Err(_) => Err(ServerFnError::::ServerError("Invalid release date".to_string())), - } - }, - Err(e) => Err(ServerFnError::::ServerError(format!("Error reading release date: {e}"))), - } + match release_date { + Ok(release_date) => Ok(Some(release_date)), + Err(_) => Err(ServerFnError::::ServerError( + "Invalid release date".to_string(), + )), + } + } + Err(e) => Err(ServerFnError::::ServerError(format!( + "Error reading release date: {e}" + ))), + } } /// Handle the file upload form #[server(input = MultipartFormData, endpoint = "/upload")] pub async fn upload(data: MultipartData) -> Result<(), ServerFnError> { - // 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(); + // 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 mut title = None; - let mut artist_ids = None; - let mut album_id = None; - let mut track = None; - let mut release_date = None; - let mut file_name = None; - let mut duration = None; + let mut title = None; + let mut artist_ids = None; + let mut album_id = None; + let mut track = None; + let mut release_date = None; + let mut file_name = None; + let mut duration = None; - // Fetch the fields from the form data - while let Ok(Some(mut field)) = data.next_field().await { - let name = field.name().unwrap_or_default().to_string(); + // Fetch the fields from the form data + while let Ok(Some(mut field)) = data.next_field().await { + let name = field.name().unwrap_or_default().to_string(); - match name.as_str() { - "title" => { title = Some(extract_field(field).await?); }, - "artist_ids" => { artist_ids = Some(validate_artist_ids(field).await?); }, - "album_id" => { album_id = Some(validate_album_id(field).await?); }, - "track_number" => { track = Some(validate_track_number(field).await?); }, - "release_date" => { release_date = Some(validate_release_date(field).await?); }, - "file" => { - use symphonia::core::codecs::CODEC_TYPE_MP3; - use crate::util::audio::extract_metadata; - use std::fs::OpenOptions; - use std::io::{Seek, Write}; + match name.as_str() { + "title" => { + title = Some(extract_field(field).await?); + } + "artist_ids" => { + artist_ids = Some(validate_artist_ids(field).await?); + } + "album_id" => { + album_id = Some(validate_album_id(field).await?); + } + "track_number" => { + track = Some(validate_track_number(field).await?); + } + "release_date" => { + release_date = Some(validate_release_date(field).await?); + } + "file" => { + use crate::util::audio::extract_metadata; + use std::fs::OpenOptions; + use std::io::{Seek, Write}; + use symphonia::core::codecs::CODEC_TYPE_MP3; - // Some logging is done here where there is high potential for bugs / failures, - // or behavior that we may wish to change in the future + // Some logging is done here where there is high potential for bugs / failures, + // 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()))?; + // Create file name + let title = title + .clone() + .ok_or(ServerFnError::::ServerError( + "Title field required 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(); - let upload_path = format!("assets/audio/upload-{date_str}_{clean_title}.mp3"); - file_name = Some(format!("upload-{date_str}_{clean_title}.mp3")); + let clean_title = title.replace(" ", "_").replace("/", "_"); + let date_str = chrono::Utc::now().format("%Y-%m-%d_%H:%M:%S").to_string(); + let upload_path = format!("assets/audio/upload-{date_str}_{clean_title}.mp3"); + file_name = Some(format!("upload-{date_str}_{clean_title}.mp3")); - debug!("Saving uploaded file {}", upload_path); + debug!("Saving uploaded file {}", upload_path); - // Save file to disk - // Use these open options to create the file, write to it, then read from it - let mut file = OpenOptions::new() - .read(true) - .write(true) - .create(true) - .truncate(true) - .open(upload_path.clone())?; + // Save file to disk + // Use these open options to create the file, write to it, then read from it + let mut file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(true) + .open(upload_path.clone())?; - while let Some(chunk) = field.chunk().await? { - file.write_all(&chunk)?; - } + while let Some(chunk) = field.chunk().await? { + file.write_all(&chunk)?; + } - file.flush()?; + file.flush()?; - // Rewind the file so the duration can be measured - file.rewind()?; + // Rewind the file so the duration can be measured + file.rewind()?; - // 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) - })?; + // 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) + })?; - if file_codec != CODEC_TYPE_MP3 { - let msg = format!("Invalid uploaded audio file codec: {file_codec}"); - warn!("{}", msg); - return Err(ServerFnError::::ServerError(msg)); - } + if file_codec != CODEC_TYPE_MP3 { + let msg = format!("Invalid uploaded audio file codec: {file_codec}"); + warn!("{}", msg); + return Err(ServerFnError::::ServerError(msg)); + } - duration = Some(file_duration); - }, - _ => { - warn!("Unknown file upload field: {}", name); - } - } - } + duration = Some(file_duration); + } + _ => { + warn!("Unknown file upload field: {}", name); + } + } + } - // Unwrap mandatory fields - let title = title.ok_or(ServerFnError::::ServerError("Missing 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}")))?; + // Unwrap mandatory fields + let title = title.ok_or(ServerFnError::::ServerError( + "Missing 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 album_id = album_id.unwrap_or(None); - let track = track.unwrap_or(None); - let release_date = release_date.unwrap_or(None); + 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("Album id and track number must both be present or both be absent".to_string())); - } + if album_id.is_some() != track.is_some() { + return Err(ServerFnError::::ServerError( + "Album id and track number must both be present or both be absent".to_string(), + )); + } - // Create the song - use crate::models::backend::Song; - let song = Song { - id: None, - title, - album_id, - track, - duration, - release_date, - storage_path: file_name, - image_path: None, - added_date: None, // Defaults to current date - }; + // Create the song + use crate::models::backend::Song; + let song = Song { + id: None, + title, + album_id, + track, + duration, + release_date, + storage_path: file_name, + image_path: None, + added_date: None, // Defaults to current date + }; - // Save the song to the database - let db_con = &mut get_db_conn(); - 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) - })?; + // Save the song to the database + let db_con = &mut get_db_conn(); + 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) + })?; - // Save the song's artists to the database - let song_id = song.id.ok_or_else(|| { - let msg = "Error saving song to database: song id not found after insertion".to_string(); - warn!("{}", msg); - ServerFnError::::ServerError(msg) - })?; + // Save the song's artists to the database + let song_id = song.id.ok_or_else(|| { + let msg = "Error saving song to database: song id not found after insertion".to_string(); + warn!("{}", msg); + ServerFnError::::ServerError(msg) + })?; - use crate::schema::song_artists; - use diesel::ExpressionMethods; + use crate::schema::song_artists; + use diesel::ExpressionMethods; - let artist_ids = artist_ids.into_iter().map(|artist_id| { - (song_artists::song_id.eq(song_id), song_artists::artist_id.eq(artist_id)) - }).collect::>(); + let artist_ids = artist_ids + .into_iter() + .map(|artist_id| { + ( + song_artists::song_id.eq(song_id), + song_artists::artist_id.eq(artist_id), + ) + }) + .collect::>(); - 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) - })?; + 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) + })?; - Ok(()) + Ok(()) } diff --git a/src/api/users.rs b/src/api/users.rs index 752c577..933682c 100644 --- a/src/api/users.rs +++ b/src/api/users.rs @@ -1,142 +1,183 @@ cfg_if::cfg_if! { - if #[cfg(feature = "ssr")] { - use diesel::prelude::*; - use crate::util::database::get_db_conn; + if #[cfg(feature = "ssr")] { + use diesel::prelude::*; + use crate::util::database::get_db_conn; - use pbkdf2::{ - password_hash::{ - rand_core::OsRng, - PasswordHasher, PasswordHash, SaltString, PasswordVerifier, Error - }, - Pbkdf2 - }; - } + use pbkdf2::{ + password_hash::{ + rand_core::OsRng, + PasswordHasher, PasswordHash, SaltString, PasswordVerifier, Error + }, + Pbkdf2 + }; + } } -use leptos::prelude::*; -use serde::{Serialize, Deserialize}; use crate::models::backend::User; +use leptos::prelude::*; +use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct UserCredentials { - pub username_or_email: String, - pub password: String + pub username_or_email: String, + pub password: String, } /// 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> { - use crate::schema::users::dsl::*; - use leptos::server_fn::error::NoCustomError; + 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(); - let user = users.filter(username.eq(username_or_email.clone())).or_filter(email.eq(username_or_email)) - .first::(db_con).optional() - .map_err(|e| ServerFnError::::ServerError(format!("Error getting user from database: {e}")))?; + // 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(); + let user = users + .filter(username.eq(username_or_email.clone())) + .or_filter(email.eq(username_or_email)) + .first::(db_con) + .optional() + .map_err(|e| { + ServerFnError::::ServerError(format!( + "Error getting user from database: {e}" + )) + })?; - Ok(user) + Ok(user) } /// 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> { - use crate::schema::users::dsl::*; - use leptos::server_fn::error::NoCustomError; + 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}")))?; + 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}" + )) + })?; - Ok(user) + Ok(user) } /// 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: &User) -> Result<(), ServerFnError> { - use crate::schema::users::dsl::*; - use leptos::server_fn::error::NoCustomError; + 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)))?; + let new_password = + new_user + .password + .clone() + .ok_or(ServerFnError::::ServerError(format!( + "No password provided for user {}", + new_user.username + )))?; - 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()))?.to_string(); - - let new_user = User { - password: Some(password_hash), - ..new_user.clone() - }; - - let db_con = &mut get_db_conn(); + 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()) + })? + .to_string(); - diesel::insert_into(users).values(&new_user).execute(db_con) - .map_err(|e| ServerFnError::::ServerError(format!("Error creating user: {e}")))?; + let new_user = User { + password: Some(password_hash), + ..new_user.clone() + }; - Ok(()) + let db_con = &mut get_db_conn(); + + diesel::insert_into(users) + .values(&new_user) + .execute(db_con) + .map_err(|e| { + ServerFnError::::ServerError(format!("Error creating user: {e}")) + })?; + + Ok(()) } /// 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; + use leptos::server_fn::error::NoCustomError; - let db_user = find_user(credentials.username_or_email.clone()).await - .map_err(|e| ServerFnError::::ServerError(format!("Error getting user from database: {e}")))?; + let db_user = find_user(credentials.username_or_email.clone()) + .await + .map_err(|e| { + ServerFnError::::ServerError(format!( + "Error getting user from database: {e}" + )) + })?; - // If the user is not found, return None - let db_user = match db_user { - Some(user) => user, - None => return Ok(None) - }; + // If the user is not found, return None + let db_user = match db_user { + Some(user) => user, + 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(ServerFnError::::ServerError(format!( + "No password found for user {}", + db_user.username + )))?; - 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| { + ServerFnError::::ServerError(format!("Error hashing supplied password: {e}")) + })?; - match Pbkdf2.verify_password(credentials.password.as_bytes(), &password_hash) { - Ok(()) => {}, - Err(Error::Password) => { - return Ok(None); - }, - Err(e) => { - return Err(ServerFnError::::ServerError(format!("Error verifying password: {e}"))); - } - } + match Pbkdf2.verify_password(credentials.password.as_bytes(), &password_hash) { + Ok(()) => {} + Err(Error::Password) => { + return Ok(None); + } + Err(e) => { + return Err(ServerFnError::::ServerError(format!( + "Error verifying password: {e}" + ))); + } + } - Ok(Some(db_user)) + Ok(Some(db_user)) } /// 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")] pub async fn get_user(username_or_email: String) -> Result, ServerFnError> { - let mut user = find_user(username_or_email).await?; + let mut user = find_user(username_or_email).await?; - // Remove the password hash before returning the user - if let Some(user) = user.as_mut() { - user.password = None; - } + // Remove the password hash before returning the user + if let Some(user) = user.as_mut() { + user.password = None; + } - Ok(user) + Ok(user) } #[server(endpoint = "get_user_by_id")] pub async fn get_user_by_id(user_id: i32) -> Result, ServerFnError> { - let mut user = find_user_by_id(user_id).await?; + let mut user = find_user_by_id(user_id).await?; - // Remove the password hash before returning the user - if let Some(user) = user.as_mut() { - user.password = None; - } + // Remove the password hash before returning the user + if let Some(user) = user.as_mut() { + user.password = None; + } - Ok(user) + Ok(user) } diff --git a/src/app.rs b/src/app.rs index 762a73e..c985c3d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,20 +1,20 @@ -use crate::components::playbar::PlayBar; +use crate::components::error_template::{AppError, ErrorTemplate}; use crate::components::playbar::CustomTitle; +use crate::components::playbar::PlayBar; use crate::components::queue::Queue; -use leptos::prelude::*; -use leptos_meta::*; -use leptos_router::*; -use leptos_router::components::*; -use crate::pages::login::*; -use crate::pages::signup::*; -use crate::pages::profile::*; use crate::pages::album::*; use crate::pages::artist::*; -use crate::pages::song::*; -use crate::pages::search::*; use crate::pages::dashboard::*; -use crate::components::error_template::{AppError, ErrorTemplate}; +use crate::pages::login::*; +use crate::pages::profile::*; +use crate::pages::search::*; +use crate::pages::signup::*; +use crate::pages::song::*; use crate::util::state::GlobalState; +use leptos::prelude::*; +use leptos_meta::*; +use leptos_router::components::*; +use leptos_router::*; pub fn shell(options: LeptosOptions) -> impl IntoView { view! { @@ -82,15 +82,19 @@ pub fn App() -> impl IntoView { } } -use crate::components::sidebar::*; -use crate::components::personal::Personal; -use crate::components::upload::*; -use crate::components::add_artist::AddArtist; use crate::components::add_album::AddAlbum; +use crate::components::add_artist::AddArtist; +use crate::components::personal::Personal; +use crate::components::sidebar::*; +use crate::components::upload::*; /// Renders the home page of your application. #[component] -fn HomePage(upload_open: RwSignal, add_artist_open: RwSignal, add_album_open: RwSignal) -> impl IntoView { +fn HomePage( + upload_open: RwSignal, + add_artist_open: RwSignal, + add_album_open: RwSignal, +) -> impl IntoView { view! {
diff --git a/src/components/add_album.rs b/src/components/add_album.rs index 5a29cef..8af5c71 100644 --- a/src/components/add_album.rs +++ b/src/components/add_album.rs @@ -1,8 +1,8 @@ -use leptos::prelude::*; -use leptos::leptos_dom::log; -use leptos_icons::*; -use leptos::task::spawn_local; use crate::api::albums::add_album; +use leptos::leptos_dom::log; +use leptos::prelude::*; +use leptos::task::spawn_local; +use leptos_icons::*; #[component] pub fn AddAlbumBtn(add_album_open: RwSignal) -> impl IntoView { @@ -22,9 +22,9 @@ pub fn AddAlbum(open: RwSignal) -> impl IntoView { let image_path = RwSignal::new("".to_string()); let close_dialog = move |ev: leptos::ev::MouseEvent| { - ev.prevent_default(); - open.set(false); - }; + ev.prevent_default(); + open.set(false); + }; let on_add_album = move |ev: leptos::ev::SubmitEvent| { ev.prevent_default(); @@ -33,7 +33,8 @@ pub fn AddAlbum(open: RwSignal) -> impl IntoView { let image_path_clone = Some(image_path.get()); spawn_local(async move { - let add_album_result = add_album(album_title_clone, release_date_clone, image_path_clone).await; + let add_album_result = + add_album(album_title_clone, release_date_clone, image_path_clone).await; if let Err(err) = add_album_result { log!("Error adding album: {:?}", err); } else if let Ok(album) = add_album_result { @@ -54,32 +55,32 @@ pub fn AddAlbum(open: RwSignal) -> impl IntoView {
- Album Title
-
- Release - Date -
- + Release + Date +
+ - +
- Image Path
@@ -88,5 +89,4 @@ pub fn AddAlbum(open: RwSignal) -> impl IntoView { } - -} \ No newline at end of file +} diff --git a/src/components/add_artist.rs b/src/components/add_artist.rs index 524922d..89888f0 100644 --- a/src/components/add_artist.rs +++ b/src/components/add_artist.rs @@ -1,8 +1,8 @@ -use leptos::prelude::*; -use leptos::leptos_dom::log; -use leptos_icons::*; -use leptos::task::spawn_local; use crate::api::artists::add_artist; +use leptos::leptos_dom::log; +use leptos::prelude::*; +use leptos::task::spawn_local; +use leptos_icons::*; #[component] pub fn AddArtistBtn(add_artist_open: RwSignal) -> impl IntoView { @@ -20,9 +20,9 @@ pub fn AddArtist(open: RwSignal) -> impl IntoView { let artist_name = RwSignal::new("".to_string()); let close_dialog = move |ev: leptos::ev::MouseEvent| { - ev.prevent_default(); - open.set(false); - }; + ev.prevent_default(); + open.set(false); + }; let on_add_artist = move |ev: leptos::ev::SubmitEvent| { ev.prevent_default(); let artist_name_clone = artist_name.get(); @@ -46,11 +46,11 @@ pub fn AddArtist(open: RwSignal) -> impl IntoView {
- Artist Name
@@ -59,4 +59,4 @@ pub fn AddArtist(open: RwSignal) -> impl IntoView { } -} \ No newline at end of file +} diff --git a/src/components/dashboard_row.rs b/src/components/dashboard_row.rs index fd76bbe..8b39ff7 100644 --- a/src/components/dashboard_row.rs +++ b/src/components/dashboard_row.rs @@ -1,86 +1,89 @@ +use crate::components::dashboard_tile::*; use leptos::html::Ul; use leptos::leptos_dom::*; use leptos::prelude::*; use leptos::text_prop::TextProp; -use leptos_use::{use_element_size, UseElementSizeReturn, use_scroll, UseScrollReturn}; -use crate::components::dashboard_tile::*; use leptos_icons::*; +use leptos_use::{use_element_size, use_scroll, UseElementSizeReturn, UseScrollReturn}; /// A row of dashboard tiles, with a title #[component] pub fn DashboardRow( - #[prop(into)] title: TextProp, - #[prop(default=vec![])] tiles: Vec, + #[prop(into)] title: TextProp, + #[prop(default=vec![])] tiles: Vec, ) -> impl IntoView { - let list_ref = NodeRef::