Merge branch '31-update-songdata-for-fetching-songs-on-frontend-for-playback' into 'main'

Resolve "Update SongData for fetching songs on frontend for playback"

Closes #31

See merge request libretunes/libretunes!31
This commit is contained in:
Ethan Girouard 2024-07-24 17:18:10 -04:00
commit 2ea3979a89
4 changed files with 141 additions and 11 deletions

View File

@ -164,6 +164,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
@ -293,4 +313,30 @@ 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)
}
}
} }

View File

@ -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,19 +244,20 @@ 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()))
}) })
}); });
@ -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");

View File

@ -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!{

View File

@ -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)
},
})
}
}