Move data types into models/frontend and models/backend

This commit is contained in:
2025-02-05 12:34:48 -05:00
parent d72ed532c1
commit e42247ee84
36 changed files with 959 additions and 888 deletions

View File

@ -1,6 +1,5 @@
use leptos::prelude::*;
use crate::albumdata::AlbumData;
use crate::songdata::SongData;
use crate::models::frontend;
use cfg_if::cfg_if;
@ -12,8 +11,8 @@ cfg_if! {
}
#[server(endpoint = "album/get")]
pub async fn get_album(id: i32) -> Result<AlbumData, ServerFnError> {
use crate::models::Album;
pub async fn get_album(id: i32) -> Result<frontend::Album, ServerFnError> {
use crate::models::backend::Album;
let db_con = &mut get_db_conn();
let album = Album::get_album_data(id,db_con)
.map_err(|e| ServerFnError::<NoCustomError>::ServerError(format!("Error getting album: {}", e)))?;
@ -21,8 +20,8 @@ pub async fn get_album(id: i32) -> Result<AlbumData, ServerFnError> {
}
#[server(endpoint = "album/get_songs")]
pub async fn get_songs(id: i32) -> Result<Vec<SongData>, ServerFnError> {
use crate::models::Album;
pub async fn get_songs(id: i32) -> Result<Vec<frontend::Song>, ServerFnError> {
use crate::models::backend::Album;
use crate::auth::get_logged_in_user;
let user = get_logged_in_user().await?;
let db_con = &mut get_db_conn();

View File

@ -24,7 +24,7 @@ cfg_if! {
#[server(endpoint = "albums/add-album")]
pub async fn add_album(album_title: String, release_date: Option<String>, image_path: Option<String>) -> Result<(), ServerFnError> {
use crate::schema::albums::{self};
use crate::models::Album;
use crate::models::backend::Album;
use leptos::server_fn::error::NoCustomError;
let parsed_release_date = match release_date {

View File

@ -2,9 +2,8 @@ use leptos::prelude::*;
use cfg_if::cfg_if;
use crate::albumdata::AlbumData;
use crate::models::Artist;
use crate::songdata::SongData;
use crate::models::frontend;
use crate::models::backend::Artist;
cfg_if! {
if #[cfg(feature = "ssr")] {
@ -12,7 +11,7 @@ cfg_if! {
use diesel::prelude::*;
use std::collections::HashMap;
use server_fn::error::NoCustomError;
use crate::models::Album;
use crate::models::backend::Album;
}
}
@ -28,7 +27,6 @@ cfg_if! {
#[server(endpoint = "artists/add-artist")]
pub async fn add_artist(artist_name: String) -> Result<(), ServerFnError> {
use crate::schema::artists::dsl::*;
use crate::models::Artist;
use leptos::server_fn::error::NoCustomError;
let new_artist = Artist {
@ -48,7 +46,6 @@ pub async fn add_artist(artist_name: String) -> Result<(), ServerFnError> {
#[server(endpoint = "artists/get")]
pub async fn get_artist_by_id(artist_id: i32) -> Result<Option<Artist>, ServerFnError> {
use crate::schema::artists::dsl::*;
use crate::models::Artist;
use leptos::server_fn::error::NoCustomError;
let db = &mut get_db_conn();
@ -62,8 +59,8 @@ pub async fn get_artist_by_id(artist_id: i32) -> Result<Option<Artist>, ServerFn
}
#[server(endpoint = "artists/top_songs")]
pub async fn top_songs_by_artist(artist_id: i32, limit: Option<i64>) -> Result<Vec<(SongData, i64)>, ServerFnError> {
use crate::models::Song;
pub async fn top_songs_by_artist(artist_id: i32, limit: Option<i64>) -> Result<Vec<(frontend::Song, i64)>, ServerFnError> {
use crate::models::backend::Song;
use crate::auth::get_user;
use crate::schema::*;
use leptos::server_fn::error::NoCustomError;
@ -114,7 +111,7 @@ pub async fn top_songs_by_artist(artist_id: i32, limit: Option<i64>) -> Result<V
))
.load(db)?;
let mut top_songs_map: HashMap<i32, (SongData, i64)> = HashMap::with_capacity(top_songs.len());
let mut top_songs_map: HashMap<i32, (frontend::Song, i64)> = HashMap::with_capacity(top_songs.len());
for (song, album, artist, like, dislike) in top_songs {
let song_id = song.id
@ -137,7 +134,7 @@ pub async fn top_songs_by_artist(artist_id: i32, limit: Option<i64>) -> Result<V
album.as_ref().map(|album| album.image_path.clone()).flatten()
.unwrap_or("/assets/images/placeholders/MusicPlaceholder.svg".to_string()));
let songdata = SongData {
let songdata = frontend::Song {
id: song_id,
title: song.title,
artists: artist.map(|artist| vec![artist]).unwrap_or_default(),
@ -158,13 +155,13 @@ pub async fn top_songs_by_artist(artist_id: i32, limit: Option<i64>) -> Result<V
}
}
let mut top_songs: Vec<(SongData, i64)> = top_songs_map.into_iter().map(|(_, v)| v).collect();
let mut top_songs: Vec<(frontend::Song, i64)> = top_songs_map.into_iter().map(|(_, v)| v).collect();
top_songs.sort_by(|(_, plays1), (_, plays2)| plays2.cmp(plays1));
Ok(top_songs)
}
#[server(endpoint = "artists/albums")]
pub async fn albums_by_artist(artist_id: i32, limit: Option<i64>) -> Result<Vec<AlbumData>, ServerFnError> {
pub async fn albums_by_artist(artist_id: i32, limit: Option<i64>) -> Result<Vec<frontend::Album>, ServerFnError> {
use crate::schema::*;
let db = &mut get_db_conn();
@ -186,7 +183,7 @@ pub async fn albums_by_artist(artist_id: i32, limit: Option<i64>) -> Result<Vec<
.load(db)?
};
let mut albums_map: HashMap<i32, AlbumData> = HashMap::with_capacity(album_ids.len());
let mut albums_map: HashMap<i32, frontend::Album> = HashMap::with_capacity(album_ids.len());
let album_artists: Vec<(Album, Artist)> = albums::table
.filter(albums::id.eq_any(album_ids))
@ -201,7 +198,7 @@ pub async fn albums_by_artist(artist_id: i32, limit: Option<i64>) -> Result<Vec<
if let Some(stored_album) = albums_map.get_mut(&album_id) {
stored_album.artists.push(artist);
} else {
let albumdata = AlbumData {
let albumdata = frontend::Album {
id: album_id,
title: album.title,
artists: vec![artist],
@ -213,7 +210,7 @@ pub async fn albums_by_artist(artist_id: i32, limit: Option<i64>) -> Result<Vec<
}
}
let mut albums: Vec<AlbumData> = albums_map.into_iter().map(|(_, v)| v).collect();
let mut albums: Vec<frontend::Album> = albums_map.into_iter().map(|(_, v)| v).collect();
albums.sort_by(|a1, a2| a2.release_date.cmp(&a1.release_date));
Ok(albums)
}

View File

@ -1,7 +1,7 @@
use chrono::NaiveDateTime;
use leptos::prelude::*;
use crate::models::HistoryEntry;
use crate::models::Song;
use crate::models::backend::HistoryEntry;
use crate::models::backend::Song;
use cfg_if::cfg_if;

View File

@ -3,9 +3,7 @@ use server_fn::codec::{MultipartData, MultipartFormData};
use cfg_if::cfg_if;
use crate::songdata::SongData;
use crate::artistdata::ArtistData;
use crate::models::frontend;
use chrono::NaiveDateTime;
cfg_if! {
@ -16,7 +14,7 @@ cfg_if! {
use crate::util::database::get_db_conn;
use diesel::prelude::*;
use diesel::dsl::count;
use crate::models::*;
use crate::models::backend::{Album, Artist, Song, HistoryEntry};
use crate::schema::*;
use std::collections::HashMap;
@ -67,7 +65,7 @@ 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<i64>) -> Result<Vec<(NaiveDateTime, SongData)>, ServerFnError> {
pub async fn recent_songs(for_user_id: i32, limit: Option<i64>) -> Result<Vec<(NaiveDateTime, frontend::Song)>, ServerFnError> {
let viewing_user_id = get_user().await
.map_err(|e| ServerFnError::<NoCustomError>::ServerError(format!("Error getting user: {}", e)))?.id.unwrap();
@ -111,7 +109,7 @@ pub async fn recent_songs(for_user_id: i32, limit: Option<i64>) -> Result<Vec<(N
.load(&mut db_con)?;
// Process the history data into a map of song ids to song data
let mut history_songs: HashMap<i32, (NaiveDateTime, SongData)> = HashMap::with_capacity(history.len());
let mut history_songs: HashMap<i32, (NaiveDateTime, frontend::Song)> = HashMap::with_capacity(history.len());
for (history, song, album, artist, like, dislike) in history {
let song_id = history.song_id;
@ -133,7 +131,7 @@ pub async fn recent_songs(for_user_id: i32, limit: Option<i64>) -> Result<Vec<(N
album.as_ref().map(|album| album.image_path.clone()).flatten()
.unwrap_or("/assets/images/placeholders/MusicPlaceholder.svg".to_string()));
let songdata = SongData {
let songdata = frontend::Song {
id: song_id,
title: song.title,
artists: artist.map(|artist| vec![artist]).unwrap_or_default(),
@ -152,7 +150,7 @@ pub async fn recent_songs(for_user_id: i32, limit: Option<i64>) -> Result<Vec<(N
}
// Sort the songs by date
let mut history_songs: Vec<(NaiveDateTime, SongData)> = history_songs.into_values().collect();
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)
}
@ -163,7 +161,7 @@ pub async fn recent_songs(for_user_id: i32, limit: Option<i64>) -> Result<Vec<(N
/// 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: NaiveDateTime, end_date: NaiveDateTime, limit: Option<i64>)
-> Result<Vec<(i64, SongData)>, ServerFnError>
-> Result<Vec<(i64, frontend::Song)>, ServerFnError>
{ let viewing_user_id = get_user().await
.map_err(|e| ServerFnError::<NoCustomError>::ServerError(format!("Error getting user: {}", e)))?.id.unwrap();
@ -211,7 +209,7 @@ pub async fn top_songs(for_user_id: i32, start_date: NaiveDateTime, end_date: Na
.load(&mut db_con)?;
// Process the history data into a map of song ids to song data
let mut history_songs_map: HashMap<i32, (i64, SongData)> = HashMap::with_capacity(history_counts.len());
let mut history_songs_map: HashMap<i32, (i64, frontend::Song)> = HashMap::with_capacity(history_counts.len());
for (song, album, artist, like, dislike) in history_songs {
let song_id = song.id
@ -234,7 +232,7 @@ pub async fn top_songs(for_user_id: i32, start_date: NaiveDateTime, end_date: Na
album.as_ref().map(|album| album.image_path.clone()).flatten()
.unwrap_or("/assets/images/placeholders/MusicPlaceholder.svg".to_string()));
let songdata = SongData {
let songdata = frontend::Song {
id: song_id,
title: song.title,
artists: artist.map(|artist| vec![artist]).unwrap_or_default(),
@ -256,7 +254,7 @@ pub async fn top_songs(for_user_id: i32, start_date: NaiveDateTime, end_date: Na
}
// Sort the songs by play count
let mut history_songs: Vec<(i64, SongData)> = history_songs_map.into_values().collect();
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)
}
@ -267,7 +265,7 @@ pub async fn top_songs(for_user_id: i32, start_date: NaiveDateTime, end_date: Na
/// 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<i64>)
-> Result<Vec<(i64, ArtistData)>, ServerFnError>
-> Result<Vec<(i64, frontend::Artist)>, ServerFnError>
{
let mut db_con = get_db_conn();
@ -295,8 +293,8 @@ pub async fn top_artists(for_user_id: i32, start_date: NaiveDateTime, end_date:
.load(&mut db_con)?
};
let artist_data: Vec<(i64, ArtistData)> = artist_counts.into_iter().map(|(plays, artist)| {
(plays, ArtistData {
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/artists/{}.webp", artist.id.unwrap()),

View File

@ -2,15 +2,14 @@ use leptos::prelude::*;
use cfg_if::cfg_if;
use crate::songdata::SongData;
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::auth::get_user;
use crate::models::{Song, Album, Artist};
use crate::models::backend::{Song, Album, Artist};
use diesel::prelude::*;
}
}
@ -59,7 +58,7 @@ pub async fn get_like_dislike_song(song_id: i32) -> Result<(bool, bool), ServerF
}
#[server(endpoint = "songs/get")]
pub async fn get_song_by_id(song_id: i32) -> Result<Option<SongData>, ServerFnError> {
pub async fn get_song_by_id(song_id: i32) -> Result<Option<frontend::Song>, ServerFnError> {
use crate::schema::*;
let user_id: i32 = get_user().await.map_err(|e| ServerFnError::<NoCustomError>::
@ -97,7 +96,7 @@ pub async fn get_song_by_id(song_id: i32) -> Result<Option<SongData>, ServerFnEr
)
});
Ok(Some(SongData {
Ok(Some(frontend::Song {
id: song.id.unwrap(),
title: song.title.clone(),
artists: artists,

View File

@ -11,7 +11,7 @@ cfg_if! {
}
}
use crate::models::User;
use crate::models::backend::User;
use crate::users::UserCredentials;
/// Create a new user and log them in

View File

@ -2,7 +2,7 @@ use axum_login::{AuthnBackend, AuthUser, UserId};
use crate::users::UserCredentials;
use leptos::server_fn::error::ServerFnErrorErr;
use crate::models::User;
use crate::models::backend::User;
use cfg_if::cfg_if;

View File

@ -1,8 +1,8 @@
use leptos::prelude::*;
use crate::albumdata::AlbumData;
use crate::models::frontend;
#[component]
pub fn AlbumInfo(albumdata: AlbumData) -> impl IntoView {
pub fn AlbumInfo(albumdata: frontend::Album) -> impl IntoView {
view! {
<div class="album-info">
<img class="album-image" src={albumdata.image_path} alt="dashboard-tile" />

View File

@ -7,19 +7,19 @@ use leptos_icons::*;
use leptos::task::spawn_local;
use crate::api::songs::*;
use crate::songdata::SongData;
use crate::models::{Album, Artist};
use crate::models::frontend;
use crate::models::backend::{Album, Artist};
use crate::util::state::GlobalState;
const LIKE_DISLIKE_BTN_SIZE: &str = "2em";
#[component]
pub fn SongList(songs: Vec<SongData>) -> impl IntoView {
pub fn SongList(songs: Vec<frontend::Song>) -> impl IntoView {
__SongListInner(songs.into_iter().map(|song| (song, ())).collect::<Vec<_>>(), false)
}
#[component]
pub fn SongListExtra<T>(songs: Vec<(SongData, T)>) -> impl IntoView where
pub fn SongListExtra<T>(songs: Vec<(frontend::Song, T)>) -> impl IntoView where
T: Clone + IntoView + 'static
{
__SongListInner(songs, true)
@ -28,7 +28,7 @@ pub fn SongListExtra<T>(songs: Vec<(SongData, T)>) -> impl IntoView where
// TODO these arguments shouldn't need a leading underscore,
// but for some reason the compiler thinks they are unused
#[component]
fn SongListInner<T>(_songs: Vec<(SongData, T)>, _show_extra: bool) -> impl IntoView where
fn SongListInner<T>(_songs: Vec<(frontend::Song, T)>, _show_extra: bool) -> impl IntoView where
T: Clone + IntoView + 'static
{
let songs = Rc::new(_songs);
@ -41,7 +41,7 @@ fn SongListInner<T>(_songs: Vec<(SongData, T)>, _show_extra: bool) -> impl IntoV
if let Some(index) = clicked_index {
GlobalState::play_status().update(|status| {
let song: &(SongData, T) = songs.get(index).expect("Invalid song list item index");
let song: &(frontend::Song, T) = songs.get(index).expect("Invalid song list item index");
if status.queue.front().map(|song| song.id) == Some(song.0.id) {
// If the clicked song is already at the front of the queue, just play it
@ -87,7 +87,7 @@ fn SongListInner<T>(_songs: Vec<(SongData, T)>, _show_extra: bool) -> impl IntoV
}
#[component]
pub fn SongListItem<T>(song: SongData, song_playing: Signal<bool>, extra: Option<T>,
pub fn SongListItem<T>(song: frontend::Song, song_playing: Signal<bool>, extra: Option<T>,
list_index: usize, do_queue_remaining: WriteSignal<Option<usize>>) -> impl IntoView where
T: IntoView + 'static
{

View File

@ -7,8 +7,7 @@ use web_sys::Response;
use leptos::task::spawn_local;
use crate::search::search_artists;
use crate::search::search_albums;
use crate::models::Artist;
use crate::models::Album;
use crate::models::backend::{Artist, Album};
#[component]
pub fn UploadBtn(dialog_open: RwSignal<bool>) -> impl IntoView {

View File

@ -1,9 +1,5 @@
pub mod app;
pub mod auth;
pub mod songdata;
pub mod albumdata;
pub mod artistdata;
pub mod playstatus;
pub mod playbar;
pub mod queue;
pub mod song;

View File

@ -1,791 +0,0 @@
use chrono::{NaiveDate, NaiveDateTime};
use serde::{Deserialize, Serialize};
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use diesel::prelude::*;
use crate::util::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<i32>,
/// 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<String>,
/// The time the user was created
#[cfg_attr(feature = "ssr", diesel(deserialize_as = NaiveDateTime))]
pub created_at: Option<NaiveDateTime>,
/// 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<Vec<HistoryEntry>, Box<dyn Error>>` -
/// A result indicating success with a vector of history entries, or an error
///
#[cfg(feature = "ssr")]
pub fn get_history(self: &Self, limit: Option<i64>, conn: &mut PgPooledConn) ->
Result<Vec<HistoryEntry>, Box<dyn Error>> {
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<Vec<(SystemTime, Song)>, Box<dyn Error>>` -
/// 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<i64>, conn: &mut PgPooledConn) ->
Result<Vec<(NaiveDateTime, Song)>, Box<dyn Error>> {
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<dyn Error>>` - 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<dyn Error>> {
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<bool, Box<dyn Error>>` - 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<bool, Box<dyn Error>> {
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::<HistoryEntry>(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<dyn Error>> {
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<bool, Box<dyn Error>> {
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<Vec<Song>, Box<dyn Error>> {
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<dyn Error>> {
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<bool, Box<dyn Error>> {
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<Vec<Song>, Box<dyn Error>> {
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<i32>,
/// 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<dyn Error>>` - 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<dyn Error>> {
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<Vec<Album>, Box<dyn Error>>` - 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<Vec<Album>, Box<dyn Error>> {
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<dyn Error>>` - 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<dyn Error>> {
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<Vec<Song>, Box<dyn Error>>` - 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<Vec<Song>, Box<dyn Error>> {
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<Artist>) -> 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<i32>,
/// The album's title
pub title: String,
/// The album's release date
pub release_date: Option<NaiveDate>,
/// The path to the album's image file
pub image_path: Option<String>,
}
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<dyn Error>>` - 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<dyn Error>> {
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<Vec<Song>, Box<dyn Error>>` - 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<Vec<Song>, Box<dyn Error>> {
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<Album, Box<dyn Error>>` - 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<AlbumData, Box<dyn Error>> {
use crate::schema::*;
let artist_list: Vec<Artist> = album_artists::table
.filter(album_artists::album_id.eq(album_id))
.inner_join(artists::table.on(album_artists::artist_id.eq(artists::id)))
.select(
artists::all_columns
)
.load(conn)?;
// Get info of album
let albuminfo = albums::table
.filter(albums::id.eq(album_id))
.first::<Album>(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<Album, Box<dyn Error>>` - 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<User>, conn: &mut PgPooledConn) -> Result<Vec<SongData>, Box<dyn Error>> {
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<Song>, Option<Artist>, 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<Song>, Option<Artist>)> =
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<Song>, Option<Artist>, 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<i32, SongData> = 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,
added_date: song.added_date.unwrap(),
};
album_songs.insert(song.id.unwrap(), songdata);
}
}
}
// Sort the songs by date
let mut songdata: Vec<SongData> = album_songs.into_values().collect();
songdata.sort_by(|a, b| a.track.cmp(&b.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(Clone, Serialize, Deserialize)]
pub struct Song {
/// A unique id for the song
#[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))]
pub id: Option<i32>,
/// The song's title
pub title: String,
/// The album the song is from
pub album_id: Option<i32>,
/// The track number of the song on the album
pub track: Option<i32>,
/// The duration of the song in seconds
pub duration: i32,
/// The song's release date
pub release_date: Option<NaiveDate>,
/// The path to the song's audio file
pub storage_path: String,
/// The path to the song's image file
pub image_path: Option<String>,
/// The date the song was added to the database
#[cfg_attr(feature = "ssr", diesel(deserialize_as = NaiveDateTime))]
pub added_date: Option<NaiveDateTime>,
}
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<Vec<Artist>, Box<dyn Error>>` - A result indicating success with an empty value, or an error
///
#[cfg(feature = "ssr")]
pub fn get_artists(self: &Self, conn: &mut PgPooledConn) -> Result<Vec<Artist>, Box<dyn Error>> {
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<Option<Album>, Box<dyn Error>>` - 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<Option<Album>, Box<dyn Error>> {
use crate::schema::albums::dsl::*;
if let Some(album_id) = self.album_id {
let my_album = albums
.filter(id.eq(album_id))
.first::<Album>(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<i32>,
/// 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<i32>,
/// The time the playlist was created
#[cfg_attr(feature = "ssr", diesel(deserialize_as = NaiveDateTime))]
pub created_at: Option<NaiveDateTime>,
/// The time the playlist was last updated
#[cfg_attr(feature = "ssr", diesel(deserialize_as = NaiveDateTime))]
pub updated_at: Option<NaiveDateTime>,
/// The id of the user who owns the playlist
pub owner_id: i32,
/// The name of the playlist
pub name: String,
}

225
src/models/backend/album.rs Normal file
View File

@ -0,0 +1,225 @@
use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use diesel::prelude::*;
use crate::util::database::*;
use std::error::Error;
use crate::models::backend::{User, Artist, Song};
use crate::models::frontend;
}
}
/// 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<i32>,
/// The album's title
pub title: String,
/// The album's release date
pub release_date: Option<NaiveDate>,
/// The path to the album's image file
pub image_path: Option<String>,
}
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<dyn Error>>` - 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<dyn Error>> {
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<Vec<Song>, Box<dyn Error>>` - 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<Vec<Song>, Box<dyn Error>> {
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<Album, Box<dyn Error>>` - 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<frontend::Album, Box<dyn Error>> {
use crate::schema::*;
let artist_list: Vec<Artist> = album_artists::table
.filter(album_artists::album_id.eq(album_id))
.inner_join(artists::table.on(album_artists::artist_id.eq(artists::id)))
.select(
artists::all_columns
)
.load(conn)?;
// Get info of album
let albuminfo = albums::table
.filter(albums::id.eq(album_id))
.first::<Album>(conn)?;
let img = albuminfo.image_path.unwrap_or("/assets/images/placeholders/MusicPlaceholder.svg".to_string());
let albumdata = frontend::Album {
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<Album, Box<dyn Error>>` - 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<User>, conn: &mut PgPooledConn) -> Result<Vec<frontend::Song>, Box<dyn Error>> {
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<Song>, Option<Artist>, 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<Song>, Option<Artist>)> =
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<Song>, Option<Artist>, 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<i32, frontend::Song> = 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: image_path,
like_dislike: like_dislike,
added_date: song.added_date.unwrap(),
};
album_songs.insert(song.id.unwrap(), songdata);
}
}
}
// Sort the songs by date
let mut songdata: Vec<frontend::Song> = album_songs.into_values().collect();
songdata.sort_by(|a, b| a.track.cmp(&b.track));
Ok(songdata)
}
}

View File

@ -0,0 +1,153 @@
use serde::{Deserialize, Serialize};
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use diesel::prelude::*;
use crate::util::database::*;
use std::error::Error;
use crate::models::backend::{Album, Song};
}
}
/// 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<i32>,
/// 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<dyn Error>>` - 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<dyn Error>> {
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<Vec<Album>, Box<dyn Error>>` - 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<Vec<Album>, Box<dyn Error>> {
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<dyn Error>>` - 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<dyn Error>> {
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<Vec<Song>, Box<dyn Error>>` - 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<Vec<Song>, Box<dyn Error>> {
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<Artist>) -> 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
}
}

View File

@ -0,0 +1,27 @@
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use diesel::prelude::*;
}
}
/// 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<i32>,
/// 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,
}

19
src/models/backend/mod.rs Normal file
View File

@ -0,0 +1,19 @@
// 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 serverub mod user;
pub mod album;
pub mod artist;
pub mod history_entry;
pub mod playlist;
pub mod song;
pub mod user;
pub use album::Album;
pub use artist::Artist;
pub use history_entry::HistoryEntry;
pub use playlist::Playlist;
pub use song::Song;
pub use user::User;

View File

@ -0,0 +1,31 @@
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use diesel::prelude::*;
}
}
/// 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<i32>,
/// The time the playlist was created
#[cfg_attr(feature = "ssr", diesel(deserialize_as = NaiveDateTime))]
pub created_at: Option<NaiveDateTime>,
/// The time the playlist was last updated
#[cfg_attr(feature = "ssr", diesel(deserialize_as = NaiveDateTime))]
pub updated_at: Option<NaiveDateTime>,
/// The id of the user who owns the playlist
pub owner_id: i32,
/// The name of the playlist
pub name: String,
}

View File

@ -0,0 +1,98 @@
use chrono::{NaiveDate, NaiveDateTime};
use serde::{Deserialize, Serialize};
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use diesel::prelude::*;
use crate::util::database::*;
use std::error::Error;
use crate::models::backend::{Artist, Album};
}
}
/// 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(Clone, Serialize, Deserialize)]
pub struct Song {
/// A unique id for the song
#[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))]
pub id: Option<i32>,
/// The song's title
pub title: String,
/// The album the song is from
pub album_id: Option<i32>,
/// The track number of the song on the album
pub track: Option<i32>,
/// The duration of the song in seconds
pub duration: i32,
/// The song's release date
pub release_date: Option<NaiveDate>,
/// The path to the song's audio file
pub storage_path: String,
/// The path to the song's image file
pub image_path: Option<String>,
/// The date the song was added to the database
#[cfg_attr(feature = "ssr", diesel(deserialize_as = NaiveDateTime))]
pub added_date: Option<NaiveDateTime>,
}
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<Vec<Artist>, Box<dyn Error>>` - A result indicating success with an empty value, or an error
///
#[cfg(feature = "ssr")]
pub fn get_artists(self: &Self, conn: &mut PgPooledConn) -> Result<Vec<Artist>, Box<dyn Error>> {
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<Option<Album>, Box<dyn Error>>` - 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<Option<Album>, Box<dyn Error>> {
use crate::schema::albums::dsl::*;
if let Some(album_id) = self.album_id {
let my_album = albums
.filter(id.eq(album_id))
.first::<Album>(conn)?;
Ok(Some(my_album))
} else {
Ok(None)
}
}
}

310
src/models/backend/user.rs Normal file
View File

@ -0,0 +1,310 @@
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(feature = "ssr")] {
use diesel::prelude::*;
use crate::util::database::*;
use std::error::Error;
use crate::models::backend::{Song, HistoryEntry};
}
}
// 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<i32>,
/// 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<String>,
/// The time the user was created
#[cfg_attr(feature = "ssr", diesel(deserialize_as = NaiveDateTime))]
pub created_at: Option<NaiveDateTime>,
/// 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<Vec<HistoryEntry>, Box<dyn Error>>` -
/// A result indicating success with a vector of history entries, or an error
///
#[cfg(feature = "ssr")]
pub fn get_history(self: &Self, limit: Option<i64>, conn: &mut PgPooledConn) ->
Result<Vec<HistoryEntry>, Box<dyn Error>> {
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<Vec<(SystemTime, Song)>, Box<dyn Error>>` -
/// 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<i64>, conn: &mut PgPooledConn) ->
Result<Vec<(NaiveDateTime, Song)>, Box<dyn Error>> {
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<dyn Error>>` - 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<dyn Error>> {
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<bool, Box<dyn Error>>` - 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<bool, Box<dyn Error>> {
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::<HistoryEntry>(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<dyn Error>> {
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<bool, Box<dyn Error>> {
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<Vec<Song>, Box<dyn Error>> {
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<dyn Error>> {
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<bool, Box<dyn Error>> {
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<Vec<Song>, Box<dyn Error>> {
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)
}
}

View File

@ -1,4 +1,4 @@
use crate::models::Artist;
use crate::models::backend::Artist;
use crate::components::dashboard_tile::DashboardTile;
use serde::{Serialize, Deserialize};
@ -9,7 +9,7 @@ use chrono::NaiveDate;
/// Intended to be used in the front-end
#[derive(Serialize, Deserialize, Clone)]
pub struct AlbumData {
pub struct Album {
/// Album id
pub id: i32,
/// Album title
@ -23,7 +23,7 @@ pub struct AlbumData {
pub image_path: String,
}
impl Into<DashboardTile> for AlbumData {
impl Into<DashboardTile> for Album {
fn into(self) -> DashboardTile {
DashboardTile {
image_path: self.image_path.into(),

View File

@ -5,7 +5,7 @@ use serde::{Serialize, Deserialize};
///
/// Intended to be used in the front-end
#[derive(Clone, Serialize, Deserialize)]
pub struct ArtistData {
pub struct Artist {
/// Artist id
pub id: i32,
/// Artist name
@ -15,7 +15,7 @@ pub struct ArtistData {
pub image_path: String,
}
impl Into<DashboardTile> for ArtistData {
impl Into<DashboardTile> for Artist {
fn into(self) -> DashboardTile {
DashboardTile {
image_path: self.image_path.into(),

View File

@ -0,0 +1,9 @@
pub mod album;
pub mod artist;
pub mod playstatus;
pub mod song;
pub use album::Album;
pub use artist::Artist;
pub use playstatus::PlayStatus;
pub use song::Song;

View File

@ -3,7 +3,7 @@ use web_sys::HtmlAudioElement;
use leptos::html::Audio;
use std::collections::VecDeque;
use crate::songdata::SongData;
use crate::models::frontend;
/// Represents the global state of the audio player feature of LibreTunes
pub struct PlayStatus {
@ -14,9 +14,9 @@ pub struct PlayStatus {
/// A reference to the HTML audio element
pub audio_player: Option<NodeRef<Audio>>,
/// A queue of songs that have been played, ordered from oldest to newest
pub history: VecDeque<SongData>,
pub history: VecDeque<frontend::Song>,
/// A queue of songs that have yet to be played, ordered from next up to last
pub queue: VecDeque<SongData>,
pub queue: VecDeque<frontend::Song>,
}
impl PlayStatus {

View File

@ -1,4 +1,4 @@
use crate::models::{Album, Artist, Song};
use crate::models::backend::{self, Album, Artist};
use crate::components::dashboard_tile::DashboardTile;
use serde::{Serialize, Deserialize};
@ -8,7 +8,7 @@ use chrono::{NaiveDate, NaiveDateTime};
///
/// Intended to be used in the front-end, as it includes artist and album objects, rather than just their ids.
#[derive(Serialize, Deserialize, Clone)]
pub struct SongData {
pub struct Song {
/// Song id
pub id: i32,
/// Song name
@ -36,15 +36,15 @@ pub struct SongData {
}
impl TryInto<Song> for SongData {
impl TryInto<backend::Song> for Song {
type Error = Box<dyn std::error::Error>;
/// Convert a SongData object into a Song object
///
/// The SongData/Song conversions are also not truly reversible,
/// due to the way the image_path data is handled.
fn try_into(self) -> Result<Song, Self::Error> {
Ok(Song {
fn try_into(self) -> Result<backend::Song, Self::Error> {
Ok(backend::Song {
id: Some(self.id),
title: self.title,
album_id: self.album.map(|album|
@ -67,7 +67,7 @@ impl TryInto<Song> for SongData {
}
}
impl Into<DashboardTile> for SongData {
impl Into<DashboardTile> for Song {
fn into(self) -> DashboardTile {
DashboardTile {
image_path: self.image_path.into(),

2
src/models/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod backend;
pub mod frontend;

View File

@ -4,7 +4,7 @@ use leptos_router::hooks::use_params_map;
use leptos_icons::*;
use server_fn::error::NoCustomError;
use crate::models::Artist;
use crate::models::backend::Artist;
use crate::components::loading::*;
use crate::components::error::*;

View File

@ -11,7 +11,7 @@ use crate::components::error::*;
use crate::api::profile::*;
use crate::models::User;
use crate::models::backend::User;
use crate::users::get_user_by_id;
use crate::util::state::GlobalState;

View File

@ -1,5 +1,5 @@
use crate::auth::signup;
use crate::models::User;
use crate::models::backend::User;
use crate::util::state::GlobalState;
use leptos::leptos_dom::*;
use leptos::prelude::*;

View File

@ -9,7 +9,7 @@ use crate::components::loading::*;
use crate::components::error::*;
use crate::components::song_list::*;
use crate::api::songs::*;
use crate::songdata::SongData;
use crate::models::frontend;
use crate::util::state::GlobalState;
use std::rc::Rc;
@ -90,7 +90,7 @@ fn SongDetails(#[prop(into)] id: Signal<i32>) -> impl IntoView {
}
#[component]
fn SongOverview(song: SongData) -> impl IntoView {
fn SongOverview(song: frontend::Song) -> impl IntoView {
let liked = RwSignal::new(song.like_dislike.map(|ld| ld.0).unwrap_or(false));
let disliked = RwSignal::new(song.like_dislike.map(|ld| ld.1).unwrap_or(false));
@ -121,7 +121,7 @@ fn SongOverview(song: SongData) -> impl IntoView {
}
status.queue.clear();
status.queue.push_front(<Rc<SongData> as Borrow<SongData>>::borrow(&song_rc).clone());
status.queue.push_front(<Rc<frontend::Song> as Borrow<frontend::Song>>::borrow(&song_rc).clone());
status.playing = true;
}
});

View File

@ -1,5 +1,5 @@
use crate::models::Artist;
use crate::songdata::SongData;
use crate::models::backend::Artist;
use crate::models::frontend;
use crate::api::songs;
use crate::util::state::GlobalState;
use leptos::ev::MouseEvent;
@ -275,7 +275,7 @@ fn LikeDislike() -> impl IntoView {
let like_icon = Signal::derive(move || {
status.with(|status| {
match status.queue.front() {
Some(SongData { like_dislike: Some((true, _)), .. }) => icondata::TbThumbUpFilled,
Some(frontend::Song { like_dislike: Some((true, _)), .. }) => icondata::TbThumbUpFilled,
_ => icondata::TbThumbUp,
}
})
@ -284,7 +284,7 @@ fn LikeDislike() -> impl IntoView {
let dislike_icon = Signal::derive(move || {
status.with(|status| {
match status.queue.front() {
Some(SongData { like_dislike: Some((_, true)), .. }) => icondata::TbThumbDownFilled,
Some(frontend::Song { like_dislike: Some((_, true)), .. }) => icondata::TbThumbDownFilled,
_ => icondata::TbThumbDown,
}
})
@ -293,7 +293,7 @@ fn LikeDislike() -> impl IntoView {
let toggle_like = move |_| {
status.update(|status| {
match status.queue.front_mut() {
Some(SongData { id, like_dislike: Some((liked, disliked)), .. }) => {
Some(frontend::Song { id, like_dislike: Some((liked, disliked)), .. }) => {
*liked = !*liked;
if *liked {
@ -308,7 +308,7 @@ fn LikeDislike() -> impl IntoView {
}
});
},
Some(SongData { id, like_dislike, .. }) => {
Some(frontend::Song { id, like_dislike, .. }) => {
// This arm should only be reached if like_dislike is None
// In this case, the buttons will show up not filled, indicating that the song is not
// liked or disliked. Therefore, clicking the like button should like the song.
@ -333,7 +333,7 @@ fn LikeDislike() -> impl IntoView {
let toggle_dislike = move |_| {
status.update(|status| {
match status.queue.front_mut() {
Some(SongData { id, like_dislike: Some((liked, disliked)), .. }) => {
Some(frontend::Song { id, like_dislike: Some((liked, disliked)), .. }) => {
*disliked = !*disliked;
if *disliked {
@ -348,7 +348,7 @@ fn LikeDislike() -> impl IntoView {
}
});
},
Some(SongData { id, like_dislike, .. }) => {
Some(frontend::Song { id, like_dislike, .. }) => {
// This arm should only be reached if like_dislike is None
// In this case, the buttons will show up not filled, indicating that the song is not
// liked or disliked. Therefore, clicking the dislike button should dislike the song.

View File

@ -1,4 +1,4 @@
use crate::models::Artist;
use crate::models::backend::Artist;
use crate::song::Song;
use crate::util::state::GlobalState;
use leptos::ev::MouseEvent;

View File

@ -1,5 +1,5 @@
use leptos::prelude::*;
use crate::models::{Artist, Album, Song};
use crate::models::backend::{Artist, Album, Song};
use cfg_if::cfg_if;

View File

@ -29,7 +29,7 @@ async fn extract_field(field: Field<'static>) -> Result<String, ServerFnError> {
/// Expects a field with a comma-separated list of artist ids, and ensures each is a valid artist id in the database
#[cfg(feature = "ssr")]
async fn validate_artist_ids(artist_ids: Field<'static>) -> Result<Vec<i32>, ServerFnError> {
use crate::models::Artist;
use crate::models::backend::Artist;
use diesel::result::Error::NotFound;
// Extract the artist id from the field
@ -65,7 +65,7 @@ async fn validate_artist_ids(artist_ids: Field<'static>) -> Result<Vec<i32>, Ser
/// 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<Option<i32>, ServerFnError> {
use crate::models::Album;
use crate::models::backend::Album;
use diesel::result::Error::NotFound;
// Extract the album id from the field
@ -243,7 +243,7 @@ pub async fn upload(data: MultipartData) -> Result<(), ServerFnError> {
}
// Create the song
use crate::models::Song;
use crate::models::backend::Song;
let song = Song {
id: None,
title,

View File

@ -15,7 +15,7 @@ cfg_if::cfg_if! {
use leptos::prelude::*;
use serde::{Serialize, Deserialize};
use crate::models::User;
use crate::models::backend::User;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct UserCredentials {

View File

@ -1,8 +1,8 @@
use leptos::prelude::*;
use leptos::logging::*;
use crate::playstatus::PlayStatus;
use crate::models::User;
use crate::models::frontend::PlayStatus;
use crate::models::backend::User;
use crate::auth::get_logged_in_user;
/// Global front-end state