Move data types into models/frontend and models/backend
This commit is contained in:
@ -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();
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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()),
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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" />
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
791
src/models.rs
791
src/models.rs
@ -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
225
src/models/backend/album.rs
Normal 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)
|
||||
}
|
||||
}
|
153
src/models/backend/artist.rs
Normal file
153
src/models/backend/artist.rs
Normal 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
|
||||
}
|
||||
}
|
27
src/models/backend/history_entry.rs
Normal file
27
src/models/backend/history_entry.rs
Normal 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
19
src/models/backend/mod.rs
Normal 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;
|
31
src/models/backend/playlist.rs
Normal file
31
src/models/backend/playlist.rs
Normal 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,
|
||||
}
|
98
src/models/backend/song.rs
Normal file
98
src/models/backend/song.rs
Normal 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
310
src/models/backend/user.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -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(),
|
@ -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(),
|
9
src/models/frontend/mod.rs
Normal file
9
src/models/frontend/mod.rs
Normal 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;
|
@ -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 {
|
@ -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
2
src/models/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod backend;
|
||||
pub mod frontend;
|
@ -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::*;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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::*;
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user