use leptos::*; use server_fn::codec::{MultipartData, MultipartFormData}; use cfg_if::cfg_if; use crate::songdata::SongData; use crate::artistdata::ArtistData; use std::time::SystemTime; cfg_if! { if #[cfg(feature = "ssr")] { use crate::auth::get_user; use server_fn::error::NoCustomError; use crate::database::get_db_conn; use diesel::prelude::*; use diesel::dsl::count; use crate::models::*; use crate::schema::*; 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(); 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())); } // 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()))?; // Read the image, and convert it to webp use image_convert::{to_webp, WEBPConfig, ImageResource}; 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 profile_picture_path = format!("assets/images/profile/{}.webp", user_id); let mut image_target = ImageResource::from_path(&profile_picture_path); to_webp(&mut image_target, &image_source, &WEBPConfig::new()) .map_err(|e| ServerFnError::::ServerError(format!("Error converting image to webp: {}", e)))?; Ok(()) } /// Get a user's recent songs listened to /// Optionally takes a limit parameter to limit the number of songs returned. /// If not provided, all songs ever listend to are returned. /// 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 mut db_con = get_db_conn(); // Get the ids of the most recent songs listened to let history_items: Vec = if let Some(limit) = limit { song_history::table .filter(song_history::user_id.eq(for_user_id)) .order(song_history::date.desc()) .limit(limit) .select(song_history::id) .load(&mut db_con)? } else { song_history::table .filter(song_history::user_id.eq(for_user_id)) .order(song_history::date.desc()) .select(song_history::id) .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_items)) .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(for_user_id)))) .left_join(song_dislikes::table.on( songs::id.eq(song_dislikes::song_id).and(song_dislikes::user_id.eq(for_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::with_capacity(history.len()); 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, }; let image_path = song.image_path.unwrap_or( album.as_ref().map(|album| album.image_path.clone()).flatten() .unwrap_or("/assets/images/placeholders/MusicPlaceholder.svg".to_string())); let songdata = SongData { id: song_id, title: song.title, artists: artist.map(|artist| vec![artist]).unwrap_or_default(), album: album, track: song.track, duration: song.duration, release_date: song.release_date, song_path: song.storage_path, image_path: image_path, like_dislike: like_dislike, }; history_songs.insert(song_id, (history.date, songdata)); } } // Sort the songs by date let mut history_songs: Vec<(SystemTime, SongData)> = 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 /// Optionally takes a limit parameter to limit the number of songs returned. /// If not provided, all songs listened to in the date range are returned. /// Returns a list of tuples with the play count and the song data, sorted by play count (most played first). #[server(endpoint = "/profile/top_songs")] pub async fn top_songs(for_user_id: i32, start_date: SystemTime, end_date: SystemTime, limit: Option) -> Result, ServerFnError> { 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)? }; let history_counts: HashMap = history_counts.into_iter().collect(); let history_song_ids = history_counts.iter().map(|(song_id, _)| *song_id).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(for_user_id)))) .left_join(song_dislikes::table.on( songs::id.eq(song_dislikes::song_id).and(song_dislikes::user_id.eq(for_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()); 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, }; let image_path = song.image_path.unwrap_or( album.as_ref().map(|album| album.image_path.clone()).flatten() .unwrap_or("/assets/images/placeholders/MusicPlaceholder.svg".to_string())); let songdata = SongData { id: song_id, title: song.title, artists: artist.map(|artist| vec![artist]).unwrap_or_default(), album: album, track: song.track, duration: song.duration, release_date: song.release_date, song_path: song.storage_path, image_path: image_path, like_dislike: like_dislike, }; 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)); } } // Sort the songs by play count let mut history_songs: Vec<(i64, SongData)> = 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 /// Optionally takes a limit parameter to limit the number of artists returned. /// 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: SystemTime, end_date: SystemTime, 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_data: Vec<(i64, ArtistData)> = artist_counts.into_iter().map(|(plays, artist)| { (plays, ArtistData { id: artist.id.unwrap(), name: artist.name, image_path: format!("/assets/images/artists/{}.webp", artist.id.unwrap()), }) }).collect(); Ok(artist_data) }