Add functions to get top/recent songs and artists from history
This commit is contained in:
parent
f1e177c7b0
commit
23bfb510c1
@ -3,10 +3,23 @@ use server_fn::codec::{MultipartData, MultipartFormData};
|
|||||||
|
|
||||||
use cfg_if::cfg_if;
|
use cfg_if::cfg_if;
|
||||||
|
|
||||||
|
use crate::songdata::SongData;
|
||||||
|
use crate::models::Artist;
|
||||||
|
|
||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
cfg_if! {
|
cfg_if! {
|
||||||
if #[cfg(feature = "ssr")] {
|
if #[cfg(feature = "ssr")] {
|
||||||
use crate::auth::get_user;
|
use crate::auth::get_user;
|
||||||
use server_fn::error::NoCustomError;
|
use server_fn::error::NoCustomError;
|
||||||
|
|
||||||
|
use crate::database::get_db_conn;
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use diesel::dsl::count;
|
||||||
|
use crate::models::*;
|
||||||
|
use crate::schema::*;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,3 +60,233 @@ pub async fn upload_picture(data: MultipartData) -> Result<(), ServerFnError> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a user's recent songs listened to
|
||||||
|
/// Optionally takes a limit parameter to limit the number of songs returned.
|
||||||
|
/// If not provided, all songs ever listend to are returned.
|
||||||
|
/// 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<(SystemTime, SongData)>, ServerFnError> {
|
||||||
|
let mut db_con = get_db_conn();
|
||||||
|
|
||||||
|
// Get the ids of the most recent songs listened to
|
||||||
|
let history_items: Vec<i32> =
|
||||||
|
if let Some(limit) = limit {
|
||||||
|
song_history::table
|
||||||
|
.filter(song_history::user_id.eq(for_user_id))
|
||||||
|
.order(song_history::date.desc())
|
||||||
|
.limit(limit)
|
||||||
|
.select(song_history::id)
|
||||||
|
.load(&mut db_con)?
|
||||||
|
} else {
|
||||||
|
song_history::table
|
||||||
|
.filter(song_history::user_id.eq(for_user_id))
|
||||||
|
.order(song_history::date.desc())
|
||||||
|
.select(song_history::id)
|
||||||
|
.load(&mut db_con)?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Take the history ids and get the song data for them
|
||||||
|
let history: Vec<(HistoryEntry, Song, Option<Album>, Option<Artist>, Option<(i32, i32)>, Option<(i32, i32)>)>
|
||||||
|
= song_history::table
|
||||||
|
.filter(song_history::id.eq_any(history_items))
|
||||||
|
.inner_join(songs::table)
|
||||||
|
.left_join(albums::table.on(songs::album_id.eq(albums::id.nullable())))
|
||||||
|
.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(for_user_id))))
|
||||||
|
.left_join(song_dislikes::table.on(
|
||||||
|
songs::id.eq(song_dislikes::song_id).and(song_dislikes::user_id.eq(for_user_id))))
|
||||||
|
.select((
|
||||||
|
song_history::all_columns,
|
||||||
|
songs::all_columns,
|
||||||
|
albums::all_columns.nullable(),
|
||||||
|
artists::all_columns.nullable(),
|
||||||
|
song_likes::all_columns.nullable(),
|
||||||
|
song_dislikes::all_columns.nullable(),
|
||||||
|
))
|
||||||
|
.load(&mut db_con)?;
|
||||||
|
|
||||||
|
// Process the history data into a map of song ids to song data
|
||||||
|
let mut history_songs: HashMap<i32, (SystemTime, SongData)> = HashMap::with_capacity(history.len());
|
||||||
|
|
||||||
|
for (history, song, album, artist, like, dislike) in history {
|
||||||
|
let song_id = history.song_id;
|
||||||
|
|
||||||
|
if let Some((_, stored_songdata)) = history_songs.get_mut(&song_id) {
|
||||||
|
// 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.as_ref().map(|album| album.image_path.clone()).flatten()
|
||||||
|
.unwrap_or("/assets/images/placeholder.jpg".to_string()));
|
||||||
|
|
||||||
|
let songdata = SongData {
|
||||||
|
id: song_id,
|
||||||
|
title: song.title,
|
||||||
|
artists: artist.map(|artist| vec![artist]).unwrap_or_default(),
|
||||||
|
album: 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,
|
||||||
|
};
|
||||||
|
|
||||||
|
history_songs.insert(song_id, (history.date, songdata));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the songs by date
|
||||||
|
let mut history_songs: Vec<(SystemTime, SongData)> = history_songs.into_values().collect();
|
||||||
|
history_songs.sort_by(|a, b| b.0.cmp(&a.0));
|
||||||
|
Ok(history_songs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a user's top songs by play count from a date range
|
||||||
|
/// Optionally takes a limit parameter to limit the number of songs returned.
|
||||||
|
/// If not provided, all songs listened to in the date range are returned.
|
||||||
|
/// 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: SystemTime, end_date: SystemTime, limit: Option<i64>)
|
||||||
|
-> Result<Vec<(i64, SongData)>, ServerFnError>
|
||||||
|
{
|
||||||
|
let mut db_con = get_db_conn();
|
||||||
|
|
||||||
|
// Get the play count and ids of the songs listened to in the date range
|
||||||
|
let history_counts: Vec<(i32, i64)> =
|
||||||
|
if let Some(limit) = limit {
|
||||||
|
song_history::table
|
||||||
|
.filter(song_history::date.between(start_date, end_date))
|
||||||
|
.filter(song_history::user_id.eq(for_user_id))
|
||||||
|
.group_by(song_history::song_id)
|
||||||
|
.select((song_history::song_id, count(song_history::song_id)))
|
||||||
|
.order(count(song_history::song_id).desc())
|
||||||
|
.limit(limit)
|
||||||
|
.load(&mut db_con)?
|
||||||
|
} else {
|
||||||
|
song_history::table
|
||||||
|
.filter(song_history::date.between(start_date, end_date))
|
||||||
|
.filter(song_history::user_id.eq(for_user_id))
|
||||||
|
.group_by(song_history::song_id)
|
||||||
|
.select((song_history::song_id, count(song_history::song_id)))
|
||||||
|
.load(&mut db_con)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let history_counts: HashMap<i32, i64> = history_counts.into_iter().collect();
|
||||||
|
let history_song_ids = history_counts.iter().map(|(song_id, _)| *song_id).collect::<Vec<i32>>();
|
||||||
|
|
||||||
|
// Get the song data for the songs listened to in the date range
|
||||||
|
let history_songs: Vec<(Song, Option<Album>, Option<Artist>, Option<(i32, i32)>, Option<(i32, i32)>)>
|
||||||
|
= songs::table
|
||||||
|
.filter(songs::id.eq_any(history_song_ids))
|
||||||
|
.left_join(albums::table.on(songs::album_id.eq(albums::id.nullable())))
|
||||||
|
.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(for_user_id))))
|
||||||
|
.left_join(song_dislikes::table.on(
|
||||||
|
songs::id.eq(song_dislikes::song_id).and(song_dislikes::user_id.eq(for_user_id))))
|
||||||
|
.select((
|
||||||
|
songs::all_columns,
|
||||||
|
albums::all_columns.nullable(),
|
||||||
|
artists::all_columns.nullable(),
|
||||||
|
song_likes::all_columns.nullable(),
|
||||||
|
song_dislikes::all_columns.nullable(),
|
||||||
|
))
|
||||||
|
.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());
|
||||||
|
|
||||||
|
for (song, album, artist, like, dislike) in history_songs {
|
||||||
|
let song_id = song.id
|
||||||
|
.ok_or(ServerFnError::ServerError::<NoCustomError>("Song id not found in database".to_string()))?;
|
||||||
|
|
||||||
|
if let Some((_, stored_songdata)) = history_songs_map.get_mut(&song_id) {
|
||||||
|
// 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.as_ref().map(|album| album.image_path.clone()).flatten()
|
||||||
|
.unwrap_or("/assets/images/placeholder.jpg".to_string()));
|
||||||
|
|
||||||
|
let songdata = SongData {
|
||||||
|
id: song_id,
|
||||||
|
title: song.title,
|
||||||
|
artists: artist.map(|artist| vec![artist]).unwrap_or_default(),
|
||||||
|
album: 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,
|
||||||
|
};
|
||||||
|
|
||||||
|
let plays = history_counts.get(&song_id)
|
||||||
|
.ok_or(ServerFnError::ServerError::<NoCustomError>("Song id not found in history counts".to_string()))?;
|
||||||
|
|
||||||
|
history_songs_map.insert(song_id, (*plays, songdata));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the songs by play count
|
||||||
|
let mut history_songs: Vec<(i64, SongData)> = history_songs_map.into_values().collect();
|
||||||
|
history_songs.sort_by(|a, b| b.0.cmp(&a.0));
|
||||||
|
Ok(history_songs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a user's top artists by play count from a date range
|
||||||
|
/// Optionally takes a limit parameter to limit the number of artists returned.
|
||||||
|
/// If not provided, all artists listened to in the date range are returned.
|
||||||
|
/// 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: SystemTime, end_date: SystemTime, limit: Option<i64>)
|
||||||
|
-> Result<Vec<(i64, Artist)>, ServerFnError>
|
||||||
|
{
|
||||||
|
let mut db_con = get_db_conn();
|
||||||
|
|
||||||
|
let artist_counts: Vec<(i64, Artist)> =
|
||||||
|
if let Some(limit) = limit {
|
||||||
|
song_history::table
|
||||||
|
.filter(song_history::date.between(start_date, end_date))
|
||||||
|
.filter(song_history::user_id.eq(for_user_id))
|
||||||
|
.inner_join(song_artists::table.on(song_history::song_id.eq(song_artists::song_id)))
|
||||||
|
.inner_join(artists::table.on(song_artists::artist_id.eq(artists::id)))
|
||||||
|
.group_by(artists::id)
|
||||||
|
.select((count(artists::id), artists::all_columns))
|
||||||
|
.order(count(artists::id).desc())
|
||||||
|
.limit(limit)
|
||||||
|
.load(&mut db_con)?
|
||||||
|
} else {
|
||||||
|
song_history::table
|
||||||
|
.filter(song_history::date.between(start_date, end_date))
|
||||||
|
.filter(song_history::user_id.eq(for_user_id))
|
||||||
|
.inner_join(song_artists::table.on(song_history::song_id.eq(song_artists::song_id)))
|
||||||
|
.inner_join(artists::table.on(song_artists::artist_id.eq(artists::id)))
|
||||||
|
.group_by(artists::id)
|
||||||
|
.select((count(artists::id), artists::all_columns))
|
||||||
|
.order(count(artists::id).desc())
|
||||||
|
.load(&mut db_con)?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(artist_counts)
|
||||||
|
}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
use crate::models::{Album, Artist, Song};
|
use crate::models::{Album, Artist, Song};
|
||||||
|
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
use time::Date;
|
use time::Date;
|
||||||
|
|
||||||
/// Holds information about a song
|
/// Holds information about a song
|
||||||
///
|
///
|
||||||
/// Intended to be used in the front-end, as it includes artist and album objects, rather than just their ids.
|
/// Intended to be used in the front-end, as it includes artist and album objects, rather than just their ids.
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct SongData {
|
pub struct SongData {
|
||||||
/// Song id
|
/// Song id
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user