diff --git a/.env.example b/.env.example
index ef8c8dd..893f587 100644
--- a/.env.example
+++ b/.env.example
@@ -15,3 +15,6 @@ DATABASE_URL=postgresql://libretunes:password@localhost:5432/libretunes
# POSTGRES_HOST=localhost
# POSTGRES_PORT=5432
# POSTGRES_DB=libretunes
+
+LIBRETUNES_AUDIO_PATH=assets/audio
+LIBRETUNES_IMAGE_PATH=assets/images
diff --git a/.gitignore b/.gitignore
index ab27e5e..f472188 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,6 +24,7 @@ playwright/.cache/
*.jpeg
*.png
*.gif
+*.webp
# Environment variables
.env
diff --git a/Cargo.lock b/Cargo.lock
index 44b8a0a..76948b4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -378,13 +378,16 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
-version = "0.4.37"
+version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e"
+checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
+ "js-sys",
"num-traits",
+ "serde",
+ "wasm-bindgen",
"windows-targets 0.52.4",
]
@@ -690,11 +693,11 @@ checksum = "03fc05c17098f21b89bc7d98fe1dd3cce2c11c2ad8e145f2a44fe08ed28eb559"
dependencies = [
"bitflags 2.5.0",
"byteorder",
+ "chrono",
"diesel_derives",
"itoa",
"pq-sys",
"r2d2",
- "time",
]
[[package]]
@@ -1834,6 +1837,7 @@ dependencies = [
"axum",
"axum-login",
"cfg-if",
+ "chrono",
"console_error_panic_hook",
"diesel",
"diesel_migrations",
@@ -1857,7 +1861,6 @@ dependencies = [
"server_fn",
"symphonia",
"thiserror",
- "time",
"tokio",
"tower 0.5.1",
"tower-http",
diff --git a/Cargo.toml b/Cargo.toml
index 2bdcda4..88c540a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,11 +19,10 @@ wasm-bindgen = { version = "=0.2.93", default-features = false, optional = true
leptos_icons = { version = "0.3.0" }
icondata = { version = "0.3.0" }
dotenv = { version = "0.15.0", optional = true }
-diesel = { version = "2.1.4", features = ["postgres", "r2d2", "time"], default-features = false, optional = true }
+diesel = { version = "2.1.4", features = ["postgres", "r2d2", "chrono"], default-features = false, optional = true }
lazy_static = { version = "1.4.0", optional = true }
serde = { version = "1.0.195", features = ["derive"], default-features = false }
openssl = { version = "0.10.63", optional = true }
-time = { version = "0.3.34", features = ["serde"], default-features = false }
diesel_migrations = { version = "2.1.0", optional = true }
pbkdf2 = { version = "0.12.2", features = ["simple"], optional = true }
tokio = { version = "1", optional = true, features = ["rt-multi-thread"] }
@@ -42,6 +41,7 @@ flexi_logger = { version = "0.28.0", optional = true, default-features = false }
web-sys = "0.3.69"
leptos-use = "0.13.5"
image-convert = { version = "0.18.0", optional = true, default-features = false }
+chrono = { version = "0.4.38", default-features = false, features = ["serde", "clock"] }
[features]
hydrate = [
@@ -50,6 +50,7 @@ hydrate = [
"leptos_router/hydrate",
"console_error_panic_hook",
"wasm-bindgen",
+ "chrono/wasmbind",
]
ssr = [
"dep:leptos_axum",
diff --git a/docker-compose.yml b/docker-compose.yml
index c2d0865..c3d466d 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -13,8 +13,11 @@ services:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
+ LIBRETUNES_AUDIO_PATH: /assets/audio
+ LIBRETUNES_IMAGE_PATH: /assets/images
volumes:
- - libretunes-audio:/site/audio
+ - libretunes-audio:/assets/audio
+ - libretunes-images:/assets/images
depends_on:
- redis
- postgres
@@ -50,5 +53,6 @@ services:
volumes:
libretunes-audio:
+ libretunes-images:
libretunes-redis:
libretunes-postgres:
diff --git a/src/albumdata.rs b/src/albumdata.rs
index e42baea..2b86b25 100644
--- a/src/albumdata.rs
+++ b/src/albumdata.rs
@@ -1,7 +1,7 @@
use crate::models::Artist;
use crate::components::dashboard_tile::DashboardTile;
-use time::Date;
+use chrono::NaiveDate;
/// Holds information about an album
///
@@ -14,7 +14,7 @@ pub struct AlbumData {
/// Album artists
pub artists: Vec,
/// Album release date
- pub release_date: Option,
+ pub release_date: Option,
/// Path to album image, relative to the root of the web server.
/// For example, `"/assets/images/Album.jpg"`
pub image_path: String,
diff --git a/src/api/history.rs b/src/api/history.rs
index 5f6cabb..697b255 100644
--- a/src/api/history.rs
+++ b/src/api/history.rs
@@ -1,4 +1,4 @@
-use std::time::SystemTime;
+use chrono::NaiveDateTime;
use leptos::*;
use crate::models::HistoryEntry;
use crate::models::Song;
@@ -25,7 +25,7 @@ pub async fn get_history(limit: Option) -> Result, Server
/// Get the listen dates and songs of the current user.
#[server(endpoint = "history/get_songs")]
-pub async fn get_history_songs(limit: Option) -> Result, ServerFnError> {
+pub async fn get_history_songs(limit: Option) -> Result, ServerFnError> {
let user = get_user().await?;
let db_con = &mut get_db_conn();
let songs = user.get_history_songs(limit, db_con)
diff --git a/src/api/profile.rs b/src/api/profile.rs
index 790af13..f994b49 100644
--- a/src/api/profile.rs
+++ b/src/api/profile.rs
@@ -3,10 +3,23 @@ use server_fn::codec::{MultipartData, MultipartFormData};
use cfg_if::cfg_if;
+use crate::songdata::SongData;
+use crate::artistdata::ArtistData;
+
+use chrono::NaiveDateTime;
+
cfg_if! {
if #[cfg(feature = "ssr")] {
use crate::auth::get_user;
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,241 @@ pub async fn upload_picture(data: MultipartData) -> Result<(), ServerFnError> {
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) -> Result, ServerFnError> {
+ let mut db_con = get_db_conn();
+
+ // Get the ids of the most recent songs listened to
+ let history_items: Vec =
+ 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, Option, 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 = 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/placeholders/MusicPlaceholder.svg".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<(NaiveDateTime, 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: NaiveDateTime, end_date: NaiveDateTime, limit: Option)
+ -> Result, 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 = history_counts.into_iter().collect();
+ let history_song_ids = history_counts.iter().map(|(song_id, _)| *song_id).collect::>();
+
+ // Get the song data for the songs listened to in the date range
+ let history_songs: Vec<(Song, Option, Option, 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 = HashMap::with_capacity(history_counts.len());
+
+ for (song, album, artist, like, dislike) in history_songs {
+ let song_id = song.id
+ .ok_or(ServerFnError::ServerError::("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/placeholders/MusicPlaceholder.svg".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::("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: NaiveDateTime, end_date: NaiveDateTime, limit: Option)
+ -> Result, 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)?
+ };
+
+ let artist_data: Vec<(i64, ArtistData)> = artist_counts.into_iter().map(|(plays, artist)| {
+ (plays, ArtistData {
+ id: artist.id.unwrap(),
+ name: artist.name,
+ image_path: format!("/assets/images/artists/{}.webp", artist.id.unwrap()),
+ })
+ }).collect();
+
+ Ok(artist_data)
+}
diff --git a/src/app.rs b/src/app.rs
index ed9fd1b..15d877f 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -3,12 +3,17 @@ use crate::playbar::CustomTitle;
use crate::playstatus::PlayStatus;
use crate::queue::Queue;
use leptos::*;
+use leptos::logging::*;
use leptos_meta::*;
use leptos_router::*;
use crate::pages::login::*;
use crate::pages::signup::*;
+use crate::pages::profile::*;
use crate::error_template::{AppError, ErrorTemplate};
+use crate::auth::get_logged_in_user;
+use crate::models::User;
+pub type LoggedInUserResource = Resource<(), Option>;
#[component]
pub fn App() -> impl IntoView {
@@ -19,6 +24,18 @@ pub fn App() -> impl IntoView {
let play_status = create_rw_signal(play_status);
let upload_open = create_rw_signal(false);
+ // A resource that fetches the logged in user
+ // This will not automatically refetch, so any login/logout related code
+ // should call `refetch` on this resource
+ let logged_in_user: LoggedInUserResource = create_resource(|| (), |_| async {
+ get_logged_in_user().await
+ .inspect_err(|e| {
+ error!("Error getting logged in user: {:?}", e);
+ })
+ .ok()
+ .flatten()
+ });
+
view! {
// injects a stylesheet into the document
// id=leptos means cargo-leptos will hot-reload this stylesheet
@@ -42,9 +59,11 @@ pub fn App() -> impl IntoView {
+ } />
+ } />
-
-
+ } />
+ } />
@@ -54,7 +73,7 @@ pub fn App() -> impl IntoView {
use crate::components::sidebar::*;
use crate::components::dashboard::*;
use crate::components::search::*;
-use crate::components::personal::*;
+use crate::components::personal::Personal;
use crate::components::upload::*;
/// Renders the home page of your application.
diff --git a/src/artistdata.rs b/src/artistdata.rs
index e799679..401979d 100644
--- a/src/artistdata.rs
+++ b/src/artistdata.rs
@@ -1,8 +1,10 @@
use crate::components::dashboard_tile::DashboardTile;
+use serde::{Serialize, Deserialize};
/// Holds information about an artist
///
/// Intended to be used in the front-end
+#[derive(Clone, Serialize, Deserialize)]
pub struct ArtistData {
/// Artist id
pub id: i32,
diff --git a/src/auth.rs b/src/auth.rs
index 37f861f..558bfe2 100644
--- a/src/auth.rs
+++ b/src/auth.rs
@@ -57,7 +57,7 @@ pub async fn signup(new_user: User) -> Result<(), ServerFnError> {
/// Takes in a username or email and a password in plaintext
/// Returns a Result with a boolean indicating if the login was successful
#[server(endpoint = "login")]
-pub async fn login(credentials: UserCredentials) -> Result {
+pub async fn login(credentials: UserCredentials) -> Result
})
+ .collect_view()
+ }
+ }
+ >
+ {move ||
+ top_songs.get().map(|top_songs| {
+ top_songs.map(|top_songs| {
+ view! {
+
+ }
+ })
+ })
+ }
+
+
+ }
+}
+
+/// Show a list of recently played songs for a user
+#[component]
+fn RecentSongs(#[prop(into)] user_id: MaybeSignal) -> impl IntoView {
+ let recent_songs = create_resource(move || user_id.get(), |user_id| async move {
+ let recent_songs = recent_songs(user_id, Some(RECENT_SONGS_COUNT)).await;
+
+ recent_songs.map(|recent_songs| {
+ recent_songs.into_iter().map(|(_date, song)| {
+ song
+ }).collect::>()
+ })
+ });
+
+ view! {
+
"Recently Played"
+ }
+ >
+ {e.to_string()}})
+ .collect_view()
+ }
+ }
+ >
+ {move ||
+ recent_songs.get().map(|recent_songs| {
+ recent_songs.map(|recent_songs| {
+ view! {
+
+ }
+ })
+ })
+ }
+
+
+ }
+}
+
+/// Show a list of top artists for a user
+#[component]
+fn TopArtists(#[prop(into)] user_id: MaybeSignal) -> impl IntoView {
+ let top_artists = create_resource(move || user_id.get(), |user_id| async move {
+ use chrono::{Local, Duration};
+
+ let now = Local::now();
+ let start = now - Duration::seconds(HISTORY_SECS);
+ let top_artists = top_artists(user_id, start.naive_utc(), now.naive_utc(), Some(TOP_ARTISTS_COUNT)).await;
+
+ top_artists.map(|top_artists| {
+ top_artists.into_iter().map(|(_plays, artist)| {
+ artist
+ }).collect::>()
+ })
+ });
+
+ view! {
+ {format!("Top Artists {}", HISTORY_MESSAGE)}
+
+ }
+ >
+ {format!("Top Artists {}", HISTORY_MESSAGE)}
+ {move || errors.get()
+ .into_iter()
+ .map(|(_, e)| view! {
{e.to_string()}
})
+ .collect_view()
+ }
+ }
+ >
+ {move ||
+ top_artists.get().map(|top_artists| {
+ top_artists.map(|top_artists| {
+ let tiles = top_artists.into_iter().map(|artist| {
+ Box::new(artist) as Box
+ }).collect::>();
+
+ DashboardRow::new(format!("Top Artists {}", HISTORY_MESSAGE), tiles)
+ })
+ })
+ }
+
+
+ }
+}
diff --git a/src/pages/signup.rs b/src/pages/signup.rs
index f02dfab..8e9a0ac 100644
--- a/src/pages/signup.rs
+++ b/src/pages/signup.rs
@@ -3,9 +3,10 @@ use crate::models::User;
use leptos::leptos_dom::*;
use leptos::*;
use leptos_icons::*;
+use crate::app::LoggedInUserResource;
#[component]
-pub fn Signup() -> impl IntoView {
+pub fn Signup(user: LoggedInUserResource) -> impl IntoView {
let (username, set_username) = create_signal("".to_string());
let (email, set_email) = create_signal("".to_string());
let (password, set_password) = create_signal("".to_string());
@@ -19,7 +20,7 @@ pub fn Signup() -> impl IntoView {
let on_submit = move |ev: leptos::ev::SubmitEvent| {
ev.prevent_default();
- let new_user = User {
+ let mut new_user = User {
id: None,
username: username.get(),
email: email.get(),
@@ -30,10 +31,17 @@ pub fn Signup() -> impl IntoView {
log!("new user: {:?}", new_user);
spawn_local(async move {
- if let Err(err) = signup(new_user).await {
+ if let Err(err) = signup(new_user.clone()).await {
// Handle the error here, e.g., log it or display to the user
log!("Error signing up: {:?}", err);
+
+ // Since we're not sure what the state is, manually refetch the user
+ user.refetch();
} else {
+ // Manually set the user to the new user, avoiding a refetch
+ new_user.password = None;
+ user.set(Some(new_user));
+
// Redirect to the login page
log!("Signed up successfully!");
leptos_router::use_navigate()("/", Default::default());
diff --git a/src/songdata.rs b/src/songdata.rs
index 6851a85..a70707f 100644
--- a/src/songdata.rs
+++ b/src/songdata.rs
@@ -1,11 +1,13 @@
use crate::models::{Album, Artist, Song};
use crate::components::dashboard_tile::DashboardTile;
-use time::Date;
+use serde::{Serialize, Deserialize};
+use chrono::NaiveDate;
/// 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.
+#[derive(Serialize, Deserialize, Clone)]
pub struct SongData {
/// Song id
pub id: i32,
@@ -20,7 +22,7 @@ pub struct SongData {
/// The duration of the song in seconds
pub duration: i32,
/// The song's release date
- pub release_date: Option,
+ pub release_date: Option,
/// Path to song file, relative to the root of the web server.
/// For example, `"/assets/audio/Song.mp3"`
pub song_path: String,
@@ -48,7 +50,6 @@ impl TryInto for SongData {
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
diff --git a/src/upload.rs b/src/upload.rs
index ff7d30d..083b0d2 100644
--- a/src/upload.rs
+++ b/src/upload.rs
@@ -10,7 +10,7 @@ cfg_if! {
use diesel::prelude::*;
use log::*;
use server_fn::error::NoCustomError;
- use time::Date;
+ use chrono::NaiveDate;
}
}
@@ -124,15 +124,14 @@ async fn validate_track_number(track_number: Field<'static>) -> Result
) -> Result
, ServerFnError> {
+async fn validate_release_date(release_date: Field<'static>) -> Result
, ServerFnError> {
match release_date.text().await {
Ok(release_date) => {
if release_date.trim().is_empty() {
return Ok(None);
}
- let date_format = time::macros::format_description!("[year]-[month]-[day]");
- let release_date = Date::parse(&release_date.trim(), date_format);
+ let release_date = NaiveDate::parse_from_str(&release_date.trim(), "%Y-%m-%d");
match release_date {
Ok(release_date) => Ok(Some(release_date)),
@@ -181,8 +180,7 @@ pub async fn upload(data: MultipartData) -> Result<(), ServerFnError> {
ServerError("Title field required and must precede file field".to_string()))?;
let clean_title = title.replace(" ", "_").replace("/", "_");
- let date_format = time::macros::format_description!("[year]-[month]-[day]_[hour]:[minute]:[second]");
- let date_str = time::OffsetDateTime::now_utc().format(date_format).unwrap_or_default();
+ let date_str = chrono::Utc::now().format("%Y-%m-%d_%H:%M:%S").to_string();
let upload_path = format!("assets/audio/upload-{}_{}.mp3", date_str, clean_title);
file_name = Some(format!("upload-{}_{}.mp3", date_str, clean_title));
diff --git a/src/users.rs b/src/users.rs
index 2d61c69..ff5606f 100644
--- a/src/users.rs
+++ b/src/users.rs
@@ -128,3 +128,15 @@ pub async fn get_user(username_or_email: String) -> Result