Merge remote-tracking branch 'origin/31-update-songdata-for-fetching-songs-on-frontend-for-playback' into 36-implement-likes-and-dislikes
This commit is contained in:
commit
18cbb0017f
1
assets/images/placeholders/MusicPlaceholder.svg
Normal file
1
assets/images/placeholders/MusicPlaceholder.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg version="1.1" viewBox="0.0 0.0 960.0 960.0" fill="none" stroke="none" stroke-linecap="square" stroke-miterlimit="10" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg"><clipPath id="p.0"><path d="m0 0l960.0 0l0 960.0l-960.0 0l0 -960.0z" clip-rule="nonzero"/></clipPath><g clip-path="url(#p.0)"><path fill="#4032a8" d="m0 0l960.0 0l0 960.0l-960.0 0z" fill-rule="evenodd"/><path fill="#ffffff" d="m290.11365 265.52475l35.496063 0l0 471.3386l-35.496063 0z" fill-rule="evenodd"/><path fill="#ffffff" d="m115.68951 737.6639l0 0c0 -36.946594 46.99247 -66.897644 104.960625 -66.897644l0 0c57.968155 0 104.96065 29.95105 104.96065 66.897644l0 0c0 36.946533 -46.992493 66.897644 -104.96065 66.897644l0 0c-57.968155 0 -104.960625 -29.95111 -104.960625 -66.897644z" fill-rule="evenodd"/><path fill="#ffffff" d="m723.79346 175.86597l35.496033 0l0 471.33856l-35.496033 0z" fill-rule="evenodd"/><path fill="#ffffff" d="m549.3693 648.00507l0 0c0 -36.946533 46.99243 -66.897644 104.96063 -66.897644l0 0c57.96814 0 104.96057 29.95111 104.96057 66.897644l0 0c0 36.946533 -46.99243 66.897644 -104.96057 66.897644l0 0c-57.9682 0 -104.96063 -29.95111 -104.96063 -66.897644z" fill-rule="evenodd"/><path fill="#ffffff" d="m759.2711 155.4385l-0.01727295 94.79588l-427.05206 100.14052l-42.09601 -84.920654z" fill-rule="evenodd"/><path fill="#ffffff" d="m421.49164 234.64502l21.039368 89.85828l-131.40158 30.803131l-21.039368 -89.85828z" fill-rule="evenodd"/></g></svg>
|
After Width: | Height: | Size: 1.4 KiB |
1
migrations/2024-05-18-231505_add_user_admin/down.sql
Normal file
1
migrations/2024-05-18-231505_add_user_admin/down.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE users DROP COLUMN admin;
|
1
migrations/2024-05-18-231505_add_user_admin/up.sql
Normal file
1
migrations/2024-05-18-231505_add_user_admin/up.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE users ADD COLUMN admin BOOLEAN DEFAULT FALSE NOT NULL;
|
37
src/auth.rs
37
src/auth.rs
@ -21,9 +21,10 @@ use crate::users::UserCredentials;
|
|||||||
pub async fn signup(new_user: User) -> Result<(), ServerFnError> {
|
pub async fn signup(new_user: User) -> Result<(), ServerFnError> {
|
||||||
use crate::users::create_user;
|
use crate::users::create_user;
|
||||||
|
|
||||||
// Ensure the user has no id
|
// Ensure the user has no id, and is not a self-proclaimed admin
|
||||||
let new_user = User {
|
let new_user = User {
|
||||||
id: None,
|
id: None,
|
||||||
|
admin: false,
|
||||||
..new_user
|
..new_user
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -143,3 +144,37 @@ pub async fn get_user() -> Result<User, ServerFnError> {
|
|||||||
|
|
||||||
auth_session.user.ok_or(ServerFnError::<NoCustomError>::ServerError("User not logged in".to_string()))
|
auth_session.user.ok_or(ServerFnError::<NoCustomError>::ServerError("User not logged in".to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if a user is an admin
|
||||||
|
/// Returns a Result with a boolean indicating if the user is logged in and an admin
|
||||||
|
#[server(endpoint = "check_admin")]
|
||||||
|
pub async fn check_admin() -> Result<bool, ServerFnError> {
|
||||||
|
let auth_session = extract::<AuthSession<AuthBackend>>().await
|
||||||
|
.map_err(|e| ServerFnError::<NoCustomError>::ServerError(format!("Error getting auth session: {}", e)))?;
|
||||||
|
|
||||||
|
Ok(auth_session.user.as_ref().map(|u| u.admin).unwrap_or(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Require that a user is logged in and an admin
|
||||||
|
/// Returns a Result with the error message if the user is not logged in or is not an admin
|
||||||
|
/// Intended to be used at the start of a protected route, to ensure the user is logged in and an admin:
|
||||||
|
/// ```rust
|
||||||
|
/// use leptos::*;
|
||||||
|
/// use libretunes::auth::require_admin;
|
||||||
|
/// #[server(endpoint = "protected_admin_route")]
|
||||||
|
/// pub async fn protected_admin_route() -> Result<(), ServerFnError> {
|
||||||
|
/// require_admin().await?;
|
||||||
|
/// // Continue with protected route
|
||||||
|
/// Ok(())
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub async fn require_admin() -> Result<(), ServerFnError> {
|
||||||
|
check_admin().await.and_then(|is_admin| {
|
||||||
|
if is_admin {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ServerFnError::<NoCustomError>::ServerError(format!("Unauthorized")))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -41,6 +41,8 @@ pub struct User {
|
|||||||
/// The time the user was created
|
/// The time the user was created
|
||||||
#[cfg_attr(feature = "ssr", diesel(deserialize_as = SystemTime))]
|
#[cfg_attr(feature = "ssr", diesel(deserialize_as = SystemTime))]
|
||||||
pub created_at: Option<SystemTime>,
|
pub created_at: Option<SystemTime>,
|
||||||
|
/// Whether the user is an admin
|
||||||
|
pub admin: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
@ -304,6 +306,26 @@ impl Artist {
|
|||||||
|
|
||||||
Ok(my_songs)
|
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
|
/// Model for an album
|
||||||
@ -433,6 +455,32 @@ impl Song {
|
|||||||
|
|
||||||
Ok(my_artists)
|
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
|
/// Model for a history entry
|
||||||
|
@ -25,6 +25,7 @@ pub fn Signup() -> impl IntoView {
|
|||||||
email: email.get(),
|
email: email.get(),
|
||||||
password: Some(password.get()),
|
password: Some(password.get()),
|
||||||
created_at: None,
|
created_at: None,
|
||||||
|
admin: false,
|
||||||
};
|
};
|
||||||
log!("new user: {:?}", new_user);
|
log!("new user: {:?}", new_user);
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use crate::models::Artist;
|
||||||
use crate::playstatus::PlayStatus;
|
use crate::playstatus::PlayStatus;
|
||||||
use leptos::ev::MouseEvent;
|
use leptos::ev::MouseEvent;
|
||||||
use leptos::html::{Audio, Div};
|
use leptos::html::{Audio, Div};
|
||||||
@ -243,26 +244,27 @@ fn PlayDuration(elapsed_secs: MaybeSignal<i64>, total_secs: MaybeSignal<i64>) ->
|
|||||||
fn MediaInfo(status: RwSignal<PlayStatus>) -> impl IntoView {
|
fn MediaInfo(status: RwSignal<PlayStatus>) -> impl IntoView {
|
||||||
let name = Signal::derive(move || {
|
let name = Signal::derive(move || {
|
||||||
status.with(|status| {
|
status.with(|status| {
|
||||||
status.queue.front().map_or("No media playing".into(), |song| song.name.clone())
|
status.queue.front().map_or("No media playing".into(), |song| song.title.clone())
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let artist = Signal::derive(move || {
|
let artist = Signal::derive(move || {
|
||||||
status.with(|status| {
|
status.with(|status| {
|
||||||
status.queue.front().map_or("".into(), |song| song.artist.clone())
|
status.queue.front().map_or("".into(), |song| format!("{}", Artist::display_list(&song.artists)))
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let album = Signal::derive(move || {
|
let album = Signal::derive(move || {
|
||||||
status.with(|status| {
|
status.with(|status| {
|
||||||
status.queue.front().map_or("".into(), |song| song.album.clone())
|
status.queue.front().map_or("".into(), |song|
|
||||||
|
song.album.as_ref().map_or("".into(), |album| album.title.clone()))
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let image = Signal::derive(move || {
|
let image = Signal::derive(move || {
|
||||||
status.with(|status| {
|
status.with(|status| {
|
||||||
// TODO Use some default / unknown image?
|
status.queue.front().map_or("/images/placeholders/MusicPlaceholder.svg".into(),
|
||||||
status.queue.front().map_or("".into(), |song| song.image_path.clone())
|
|song| song.image_path.clone())
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -400,7 +402,7 @@ pub fn PlayBar(status: RwSignal<PlayStatus>) -> impl IntoView {
|
|||||||
status.with_untracked(|status| {
|
status.with_untracked(|status| {
|
||||||
// Start playing the first song in the queue, if available
|
// Start playing the first song in the queue, if available
|
||||||
if let Some(song) = status.queue.front() {
|
if let Some(song) = status.queue.front() {
|
||||||
log!("Starting playing with song: {}", song.name);
|
log!("Starting playing with song: {}", song.title);
|
||||||
|
|
||||||
// Don't use the set_play_src / set_playing helper function
|
// Don't use the set_play_src / set_playing helper function
|
||||||
// here because we already have access to the audio element
|
// here because we already have access to the audio element
|
||||||
@ -453,7 +455,7 @@ pub fn PlayBar(status: RwSignal<PlayStatus>) -> impl IntoView {
|
|||||||
let prev_song = status.queue.pop_front();
|
let prev_song = status.queue.pop_front();
|
||||||
|
|
||||||
if let Some(prev_song) = prev_song {
|
if let Some(prev_song) = prev_song {
|
||||||
log!("Adding song to history: {}", prev_song.name);
|
log!("Adding song to history: {}", prev_song.title);
|
||||||
status.history.push_back(prev_song);
|
status.history.push_back(prev_song);
|
||||||
} else {
|
} else {
|
||||||
log!("Queue empty, no previous song to add to history");
|
log!("Queue empty, no previous song to add to history");
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
use crate::models::Artist;
|
||||||
use crate::playstatus::PlayStatus;
|
use crate::playstatus::PlayStatus;
|
||||||
use crate::song::Song;
|
use crate::song::Song;
|
||||||
use leptos::ev::MouseEvent;
|
use leptos::ev::MouseEvent;
|
||||||
@ -98,7 +99,7 @@ pub fn Queue(status: RwSignal<PlayStatus>) -> impl IntoView {
|
|||||||
on:dragenter=move |e: DragEvent| on_drag_enter(e, index)
|
on:dragenter=move |e: DragEvent| on_drag_enter(e, index)
|
||||||
on:dragover=on_drag_over
|
on:dragover=on_drag_over
|
||||||
>
|
>
|
||||||
<Song song_image_path=song.image_path.clone() song_title=song.name.clone() song_artist=song.artist.clone() />
|
<Song song_image_path=song.image_path.clone() song_title=song.title.clone() song_artist=Artist::display_list(&song.artists) />
|
||||||
<Show
|
<Show
|
||||||
when=move || index != 0
|
when=move || index != 0
|
||||||
fallback=|| view!{
|
fallback=|| view!{
|
||||||
|
@ -59,6 +59,7 @@ diesel::table! {
|
|||||||
email -> Varchar,
|
email -> Varchar,
|
||||||
password -> Varchar,
|
password -> Varchar,
|
||||||
created_at -> Timestamp,
|
created_at -> Timestamp,
|
||||||
|
admin -> Bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,25 @@
|
|||||||
|
use crate::models::{Album, Artist, Song};
|
||||||
|
|
||||||
|
use time::Date;
|
||||||
|
|
||||||
/// Holds information about a song
|
/// Holds information about a song
|
||||||
#[derive(Debug, Clone)]
|
///
|
||||||
|
/// Intended to be used in the front-end, as it includes artist and album objects, rather than just their ids.
|
||||||
pub struct SongData {
|
pub struct SongData {
|
||||||
|
/// Song id
|
||||||
|
pub id: i32,
|
||||||
/// Song name
|
/// Song name
|
||||||
pub name: String,
|
pub title: String,
|
||||||
/// Song artist
|
/// Song artists
|
||||||
pub artist: String,
|
pub artists: Vec<Artist>,
|
||||||
/// Song album
|
/// Song album
|
||||||
pub album: String,
|
pub album: Option<Album>,
|
||||||
|
/// 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<Date>,
|
||||||
/// Path to song file, relative to the root of the web server.
|
/// Path to song file, relative to the root of the web server.
|
||||||
/// For example, `"/assets/audio/Song.mp3"`
|
/// For example, `"/assets/audio/Song.mp3"`
|
||||||
pub song_path: String,
|
pub song_path: String,
|
||||||
@ -14,3 +27,71 @@ pub struct SongData {
|
|||||||
/// For example, `"/assets/images/Song.jpg"`
|
/// For example, `"/assets/images/Song.jpg"`
|
||||||
pub image_path: String,
|
pub image_path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
impl TryInto<SongData> for Song {
|
||||||
|
type Error = Box<dyn std::error::Error>;
|
||||||
|
|
||||||
|
/// Convert a Song object into a SongData object
|
||||||
|
///
|
||||||
|
/// This conversion is expensive, as it requires database queries to get the artist and album objects.
|
||||||
|
/// The SongData/Song conversions are also not truly reversible,
|
||||||
|
/// due to the way the image_path, album, and artist data is handled.
|
||||||
|
fn try_into(self) -> Result<SongData, Self::Error> {
|
||||||
|
use crate::database;
|
||||||
|
let mut db_con = database::get_db_conn();
|
||||||
|
|
||||||
|
let album = self.get_album(&mut db_con)?;
|
||||||
|
|
||||||
|
// Use the song's image path if it exists, otherwise use the album's image path, or fallback to the placeholder
|
||||||
|
let image_path = self.image_path.clone().unwrap_or_else(|| {
|
||||||
|
album
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|album| album.image_path.clone())
|
||||||
|
.unwrap_or_else(|| "/assets/images/placeholder.jpg".to_string())
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(SongData {
|
||||||
|
id: self.id.ok_or("Song id must be present (Some) to convert to SongData")?,
|
||||||
|
title: self.title.clone(),
|
||||||
|
artists: self.get_artists(&mut db_con)?,
|
||||||
|
album: album,
|
||||||
|
track: self.track,
|
||||||
|
duration: self.duration,
|
||||||
|
release_date: self.release_date,
|
||||||
|
// TODO https://gitlab.mregirouard.com/libretunes/libretunes/-/issues/35
|
||||||
|
song_path: self.storage_path,
|
||||||
|
image_path: image_path,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryInto<Song> for SongData {
|
||||||
|
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, album, and and artist data is handled.
|
||||||
|
fn try_into(self) -> Result<Song, Self::Error> {
|
||||||
|
Ok(Song {
|
||||||
|
id: Some(self.id),
|
||||||
|
title: self.title,
|
||||||
|
album_id: self.album.map(|album|
|
||||||
|
album.id.ok_or("Album id must be present (Some) to convert to Song")).transpose()?,
|
||||||
|
track: self.track,
|
||||||
|
duration: self.duration,
|
||||||
|
release_date: self.release_date,
|
||||||
|
// TODO https://gitlab.mregirouard.com/libretunes/libretunes/-/issues/35
|
||||||
|
storage_path: self.song_path,
|
||||||
|
|
||||||
|
// Note that if the source of the image_path was the album, the image_path
|
||||||
|
// will be set to the album's image_path instead of None
|
||||||
|
image_path: if self.image_path == "/assets/images/placeholder.jpg" {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(self.image_path)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user