use chrono::{NaiveDate, NaiveDateTime}; use serde::{Deserialize, Serialize}; use cfg_if::cfg_if; cfg_if! { if #[cfg(feature = "ssr")] { use diesel::prelude::*; use crate::database::*; use std::error::Error; use crate::songdata::SongData; use crate::albumdata::AlbumData; } } // These "models" are used to represent the data in the database // Diesel uses these models to generate the SQL queries that are used to interact with the database. // These types are also used for API endpoints, for consistency. Because the file must be compiled // for both the server and the client, we use the `cfg_attr` attribute to conditionally add // diesel-specific attributes to the models when compiling for the server /// Model for a "User", used for querying the database /// Various fields are wrapped in Options, because they are not always wanted for inserts/retrieval /// Using deserialize_as makes Diesel use the specified type when deserializing from the database, /// and then call .into() to convert it into the Option #[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] #[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::users))] #[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] #[derive(Serialize, Deserialize, Clone, Debug)] pub struct User { /// A unique id for the user #[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))] // #[cfg_attr(feature = "ssr", diesel(skip_insertion))] // This feature is not yet released pub id: Option, /// The user's username pub username: String, /// The user's email pub email: String, /// The user's password, stored as a hash #[cfg_attr(feature = "ssr", diesel(deserialize_as = String))] pub password: Option, /// The time the user was created #[cfg_attr(feature = "ssr", diesel(deserialize_as = NaiveDateTime))] pub created_at: Option, /// Whether the user is an admin pub admin: bool, } impl User { /// Get the history of songs listened to by this user from the database /// /// The returned history will be ordered by date in descending order, /// and a limit of N will select the N most recent entries. /// The `id` field of this user must be present (Some) to get history /// /// # Arguments /// /// * `limit` - An optional limit on the number of history entries to return /// * `conn` - A mutable reference to a database connection /// /// # Returns /// /// * `Result, Box>` - /// A result indicating success with a vector of history entries, or an error /// #[cfg(feature = "ssr")] pub fn get_history(self: &Self, limit: Option, conn: &mut PgPooledConn) -> Result, Box> { use crate::schema::song_history::dsl::*; let my_id = self.id.ok_or("Artist id must be present (Some) to get history")?; let my_history = if let Some(limit) = limit { song_history .filter(user_id.eq(my_id)) .order(date.desc()) .limit(limit) .load(conn)? } else { song_history .filter(user_id.eq(my_id)) .load(conn)? }; Ok(my_history) } /// Get the history of songs listened to by this user from the database /// /// The returned history will be ordered by date in descending order, /// and a limit of N will select the N most recent entries. /// The `id` field of this user must be present (Some) to get history /// /// # Arguments /// /// * `limit` - An optional limit on the number of history entries to return /// * `conn` - A mutable reference to a database connection /// /// # Returns /// /// * `Result, Box>` - /// A result indicating success with a vector of listen dates and songs, or an error /// #[cfg(feature = "ssr")] pub fn get_history_songs(self: &Self, limit: Option, conn: &mut PgPooledConn) -> Result, Box> { use crate::schema::songs::dsl::*; use crate::schema::song_history::dsl::*; let my_id = self.id.ok_or("Artist id must be present (Some) to get history")?; let my_history = if let Some(limit) = limit { song_history .inner_join(songs) .filter(user_id.eq(my_id)) .order(date.desc()) .limit(limit) .select((date, songs::all_columns())) .load(conn)? } else { song_history .inner_join(songs) .filter(user_id.eq(my_id)) .order(date.desc()) .select((date, songs::all_columns())) .load(conn)? }; Ok(my_history) } /// Add a song to this user's history in the database /// /// The date of the history entry will be the current time /// The `id` field of this user must be present (Some) to add history /// /// # Arguments /// /// * `song_id` - The id of the song to add to this user's history /// * `conn` - A mutable reference to a database connection /// /// # Returns /// /// * `Result<(), Box>` - A result indicating success with an empty value, or an error /// #[cfg(feature = "ssr")] pub fn add_history(self: &Self, song_id: i32, conn: &mut PgPooledConn) -> Result<(), Box> { use crate::schema::song_history; let my_id = self.id.ok_or("Artist id must be present (Some) to add history")?; diesel::insert_into(song_history::table) .values((song_history::user_id.eq(my_id), song_history::song_id.eq(song_id))) .execute(conn)?; Ok(()) } /// Check if this user has listened to a song /// /// The `id` field of this user must be present (Some) to check history /// /// # Arguments /// /// * `song_id` - The id of the song to check if this user has listened to /// * `conn` - A mutable reference to a database connection /// /// # Returns /// /// * `Result>` - A result indicating success with a boolean value, or an error /// #[cfg(feature = "ssr")] pub fn has_listened_to(self: &Self, song_id: i32, conn: &mut PgPooledConn) -> Result> { use crate::schema::song_history::{self, user_id}; let my_id = self.id.ok_or("Artist id must be present (Some) to check history")?; let has_listened = song_history::table .filter(user_id.eq(my_id)) .filter(song_history::song_id.eq(song_id)) .first::(conn) .optional()? .is_some(); Ok(has_listened) } /// Like or unlike a song for this user /// If likeing a song, remove dislike if it exists #[cfg(feature = "ssr")] pub async fn set_like_song(self: &Self, song_id: i32, like: bool, conn: &mut PgPooledConn) -> Result<(), Box> { use log::*; debug!("Setting like for song {} to {}", song_id, like); use crate::schema::song_likes; use crate::schema::song_dislikes; let my_id = self.id.ok_or("User id must be present (Some) to like/un-like a song")?; if like { diesel::insert_into(song_likes::table) .values((song_likes::song_id.eq(song_id), song_likes::user_id.eq(my_id))) .execute(conn)?; // Remove dislike if it exists diesel::delete(song_dislikes::table.filter(song_dislikes::song_id.eq(song_id) .and(song_dislikes::user_id.eq(my_id)))) .execute(conn)?; } else { diesel::delete(song_likes::table.filter(song_likes::song_id.eq(song_id).and(song_likes::user_id.eq(my_id)))) .execute(conn)?; } Ok(()) } /// Get the like status of a song for this user #[cfg(feature = "ssr")] pub async fn get_like_song(self: &Self, song_id: i32, conn: &mut PgPooledConn) -> Result> { use crate::schema::song_likes; let my_id = self.id.ok_or("User id must be present (Some) to get like status of a song")?; let like = song_likes::table .filter(song_likes::song_id.eq(song_id).and(song_likes::user_id.eq(my_id))) .first::<(i32, i32)>(conn) .optional()? .is_some(); Ok(like) } /// Get songs liked by this user #[cfg(feature = "ssr")] pub async fn get_liked_songs(self: &Self, conn: &mut PgPooledConn) -> Result, Box> { use crate::schema::songs::dsl::*; use crate::schema::song_likes::dsl::*; let my_id = self.id.ok_or("User id must be present (Some) to get liked songs")?; let my_songs = songs .inner_join(song_likes) .filter(user_id.eq(my_id)) .select(songs::all_columns()) .load(conn)?; Ok(my_songs) } /// Dislike or remove dislike from a song for this user /// If disliking a song, remove like if it exists #[cfg(feature = "ssr")] pub async fn set_dislike_song(self: &Self, song_id: i32, dislike: bool, conn: &mut PgPooledConn) -> Result<(), Box> { use log::*; debug!("Setting dislike for song {} to {}", song_id, dislike); use crate::schema::song_likes; use crate::schema::song_dislikes; let my_id = self.id.ok_or("User id must be present (Some) to dislike/un-dislike a song")?; if dislike { diesel::insert_into(song_dislikes::table) .values((song_dislikes::song_id.eq(song_id), song_dislikes::user_id.eq(my_id))) .execute(conn)?; // Remove like if it exists diesel::delete(song_likes::table.filter(song_likes::song_id.eq(song_id) .and(song_likes::user_id.eq(my_id)))) .execute(conn)?; } else { diesel::delete(song_dislikes::table.filter(song_dislikes::song_id.eq(song_id) .and(song_dislikes::user_id.eq(my_id)))) .execute(conn)?; } Ok(()) } /// Get the dislike status of a song for this user #[cfg(feature = "ssr")] pub async fn get_dislike_song(self: &Self, song_id: i32, conn: &mut PgPooledConn) -> Result> { use crate::schema::song_dislikes; let my_id = self.id.ok_or("User id must be present (Some) to get dislike status of a song")?; let dislike = song_dislikes::table .filter(song_dislikes::song_id.eq(song_id).and(song_dislikes::user_id.eq(my_id))) .first::<(i32, i32)>(conn) .optional()? .is_some(); Ok(dislike) } /// Get songs disliked by this user #[cfg(feature = "ssr")] pub async fn get_disliked_songs(self: &Self, conn: &mut PgPooledConn) -> Result, Box> { use crate::schema::songs::dsl::*; use crate::schema::song_likes::dsl::*; let my_id = self.id.ok_or("User id must be present (Some) to get disliked songs")?; let my_songs = songs .inner_join(song_likes) .filter(user_id.eq(my_id)) .select(songs::all_columns()) .load(conn)?; Ok(my_songs) } } /// Model for an artist #[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable, Identifiable))] #[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::artists))] #[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Artist { /// A unique id for the artist #[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))] pub id: Option, /// The artist's name pub name: String, } impl Artist { /// Add an album to this artist in the database /// /// # Arguments /// /// * `new_album_id` - The id of the album to add to this artist /// * `conn` - A mutable reference to a database connection /// /// # Returns /// /// * `Result<(), Box>` - A result indicating success with an empty value, or an error /// #[cfg(feature = "ssr")] pub fn add_album(self: &Self, new_album_id: i32, conn: &mut PgPooledConn) -> Result<(), Box> { use crate::schema::album_artists::dsl::*; let my_id = self.id.ok_or("Artist id must be present (Some) to add an album")?; diesel::insert_into(album_artists) .values((album_id.eq(new_album_id), artist_id.eq(my_id))) .execute(conn)?; Ok(()) } /// Get albums by artist from the database /// /// The `id` field of this artist must be present (Some) to get albums /// /// # Arguments /// /// * `conn` - A mutable reference to a database connection /// /// # Returns /// /// * `Result, Box>` - A result indicating success with a vector of albums, or an error /// #[cfg(feature = "ssr")] pub fn get_albums(self: &Self, conn: &mut PgPooledConn) -> Result, Box> { use crate::schema::albums::dsl::*; use crate::schema::album_artists::dsl::*; let my_id = self.id.ok_or("Artist id must be present (Some) to get albums")?; let my_albums = albums .inner_join(album_artists) .filter(artist_id.eq(my_id)) .select(albums::all_columns()) .load(conn)?; Ok(my_albums) } /// Add a song to this artist in the database /// /// The `id` field of this artist must be present (Some) to add a song /// /// # Arguments /// /// * `new_song_id` - The id of the song to add to this artist /// * `conn` - A mutable reference to a database connection /// /// # Returns /// /// * `Result<(), Box>` - A result indicating success with an empty value, or an error /// #[cfg(feature = "ssr")] pub fn add_song(self: &Self, new_song_id: i32, conn: &mut PgPooledConn) -> Result<(), Box> { use crate::schema::song_artists::dsl::*; let my_id = self.id.ok_or("Artist id must be present (Some) to add an album")?; diesel::insert_into(song_artists) .values((song_id.eq(new_song_id), artist_id.eq(my_id))) .execute(conn)?; Ok(()) } /// Get songs by this artist from the database /// /// The `id` field of this artist must be present (Some) to get songs /// /// # Arguments /// /// * `conn` - A mutable reference to a database connection /// /// # Returns /// /// * `Result, Box>` - A result indicating success with a vector of songs, or an error /// #[cfg(feature = "ssr")] pub fn get_songs(self: &Self, conn: &mut PgPooledConn) -> Result, Box> { use crate::schema::songs::dsl::*; use crate::schema::song_artists::dsl::*; let my_id = self.id.ok_or("Artist id must be present (Some) to get songs")?; let my_songs = songs .inner_join(song_artists) .filter(artist_id.eq(my_id)) .select(songs::all_columns()) .load(conn)?; Ok(my_songs) } /// Display a list of artists as a string. /// /// For one artist, displays [artist1]. For two artists, displays [artist1] & [artist2]. /// For three or more artists, displays [artist1], [artist2], & [artist3]. pub fn display_list(artists: &Vec) -> String { let mut artist_list = String::new(); for (i, artist) in artists.iter().enumerate() { if i == 0 { artist_list.push_str(&artist.name); } else if i == artists.len() - 1 { artist_list.push_str(&format!(" & {}", artist.name)); } else { artist_list.push_str(&format!(", {}", artist.name)); } } artist_list } } /// Model for an album #[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable, Identifiable))] #[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::albums))] #[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Album { /// A unique id for the album #[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))] pub id: Option, /// The album's title pub title: String, /// The album's release date pub release_date: Option, /// The path to the album's image file pub image_path: Option, } impl Album { /// Add an artist to this album in the database /// /// The `id` field of this album must be present (Some) to add an artist /// /// # Arguments /// /// * `new_artist_id` - The id of the artist to add to this album /// * `conn` - A mutable reference to a database connection /// /// # Returns /// /// * `Result<(), Box>` - A result indicating success with an empty value, or an error /// #[cfg(feature = "ssr")] pub fn add_artist(self: &Self, new_artist_id: i32, conn: &mut PgPooledConn) -> Result<(), Box> { use crate::schema::album_artists::dsl::*; let my_id = self.id.ok_or("Album id must be present (Some) to add an artist")?; diesel::insert_into(album_artists) .values((album_id.eq(my_id), artist_id.eq(new_artist_id))) .execute(conn)?; Ok(()) } /// Get songs by this album from the database /// /// The `id` field of this album must be present (Some) to get songs /// /// # Arguments /// /// * `conn` - A mutable reference to a database connection /// /// # Returns /// /// * `Result, Box>` - A result indicating success with a vector of songs, or an error /// #[cfg(feature = "ssr")] pub fn get_songs(self: &Self, conn: &mut PgPooledConn) -> Result, Box> { use crate::schema::songs::dsl::*; use crate::schema::song_artists::dsl::*; let my_id = self.id.ok_or("Album id must be present (Some) to get songs")?; let my_songs = songs .inner_join(song_artists) .filter(album_id.eq(my_id)) .select(songs::all_columns()) .load(conn)?; Ok(my_songs) } /// Obtain an album from its albumid /// # Arguments /// /// * `album_id` - The id of the album to select /// * `conn` - A mutable reference to a database connection /// /// # Returns /// /// * `Result>` - A result indicating success with the desired album, or an error /// #[cfg(feature = "ssr")] pub fn get_album_data(album_id: i32, conn: &mut PgPooledConn) -> Result> { use crate::schema::*; let album: Vec<(Album, std::option::Option)> = albums::table .find(album_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, artists::all_columns.nullable() )) .distinct() .load(conn)?; let mut artist_list: Vec = Vec::new(); for (_, artist) in album { if let Some(artist) = artist { artist_list.push(artist); } } // Get info of album let albuminfo = albums::table .filter(albums::id.eq(album_id)) .first::(conn)?; let img = albuminfo.image_path.unwrap_or("/assets/images/placeholders/MusicPlaceholder.svg".to_string()); let albumdata = AlbumData { id: albuminfo.id.unwrap(), title: albuminfo.title, artists: artist_list, release_date: albuminfo.release_date, image_path: img }; Ok(albumdata) } /// Obtain an album from its albumid /// # Arguments /// /// * `album_id` - The id of the album to select /// * `conn` - A mutable reference to a database connection /// /// # Returns /// /// * `Result>` - A result indicating success with the desired album, or an error /// #[cfg(feature = "ssr")] pub fn get_song_data(album_id: i32, user_like_dislike: Option, conn: &mut PgPooledConn) -> Result, Box> { use crate::schema::*; use std::collections::HashMap; let song_list = if let Some(user_like_dislike) = user_like_dislike { let user_like_dislike_id = user_like_dislike.id.unwrap(); let song_list: Vec<(Album, Option, Option, Option<(i32, i32)>, Option<(i32, i32)>)> = albums::table .find(album_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_like_dislike_id)))) .left_join(song_dislikes::table.on(songs::id.eq(song_dislikes::song_id).and(song_dislikes::user_id.eq(user_like_dislike_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(conn)?; song_list } else { let song_list: Vec<(Album, Option, Option)> = albums::table .find(album_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(conn)?; let song_list: Vec<(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 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 = SongData { 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: image_path, like_dislike: like_dislike, }; album_songs.insert(song.id.unwrap(), songdata); } } } // Sort the songs by date let mut songdata: Vec = album_songs.into_values().collect(); songdata.sort_by(|a, b| b.track.cmp(&a.track)); Ok(songdata) } } /// Model for a song #[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] #[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::songs))] #[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] #[derive(Serialize, Deserialize)] pub struct Song { /// A unique id for the song #[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))] pub id: Option, /// The song's title pub title: String, /// The album the song is from pub album_id: Option, /// The track number of the song on the album pub track: Option, /// The duration of the song in seconds pub duration: i32, /// The song's release date pub release_date: Option, /// The path to the song's audio file pub storage_path: String, /// The path to the song's image file pub image_path: Option, } impl Song { /// Add an artist to this song in the database /// /// The `id` field of this song must be present (Some) to add an artist /// /// # Arguments /// /// * `new_artist_id` - The id of the artist to add to this song /// * `conn` - A mutable reference to a database connection /// /// # Returns /// /// * `Result, Box>` - A result indicating success with an empty value, or an error /// #[cfg(feature = "ssr")] pub fn get_artists(self: &Self, conn: &mut PgPooledConn) -> Result, Box> { use crate::schema::artists::dsl::*; use crate::schema::song_artists::dsl::*; let my_id = self.id.ok_or("Song id must be present (Some) to get artists")?; let my_artists = artists .inner_join(song_artists) .filter(song_id.eq(my_id)) .select(artists::all_columns()) .load(conn)?; Ok(my_artists) } /// Get the album for this song from the database /// /// # Arguments /// /// * `conn` - A mutable reference to a database connection /// /// # Returns /// /// * `Result, Box>` - A result indicating success with an album, or None if /// the song does not have an album, or an error /// #[cfg(feature = "ssr")] pub fn get_album(self: &Self, conn: &mut PgPooledConn) -> Result, Box> { use crate::schema::albums::dsl::*; if let Some(album_id) = self.album_id { let my_album = albums .filter(id.eq(album_id)) .first::(conn)?; Ok(Some(my_album)) } else { Ok(None) } } } /// Model for a history entry #[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] #[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::song_history))] #[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] #[derive(Serialize, Deserialize)] pub struct HistoryEntry { /// A unique id for the history entry #[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))] pub id: Option, /// The id of the user who listened to the song pub user_id: i32, /// The date the song was listened to pub date: NaiveDateTime, /// The id of the song that was listened to pub song_id: i32, } /// Model for a playlist #[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] #[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::playlists))] #[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] #[derive(Serialize, Deserialize)] pub struct Playlist { /// A unique id for the playlist #[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))] pub id: Option, /// The time the playlist was created #[cfg_attr(feature = "ssr", diesel(deserialize_as = NaiveDateTime))] pub created_at: Option, /// The time the playlist was last updated #[cfg_attr(feature = "ssr", diesel(deserialize_as = NaiveDateTime))] pub updated_at: Option, /// The id of the user who owns the playlist pub owner_id: i32, /// The name of the playlist pub name: String, }