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/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 c8c8716..f994b49 100644 --- a/src/api/profile.rs +++ b/src/api/profile.rs @@ -6,7 +6,7 @@ use cfg_if::cfg_if; use crate::songdata::SongData; use crate::artistdata::ArtistData; -use std::time::SystemTime; +use chrono::NaiveDateTime; cfg_if! { if #[cfg(feature = "ssr")] { @@ -67,7 +67,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) -> Result, ServerFnError> { +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 @@ -108,7 +108,7 @@ pub async fn recent_songs(for_user_id: i32, limit: Option) -> Result = HashMap::with_capacity(history.len()); + 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; @@ -148,7 +148,7 @@ pub async fn recent_songs(for_user_id: i32, limit: Option) -> Result = history_songs.into_values().collect(); + 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) } @@ -158,7 +158,7 @@ pub async fn recent_songs(for_user_id: i32, limit: Option) -> Result) +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(); @@ -259,7 +259,7 @@ pub async fn top_songs(for_user_id: i32, start_date: SystemTime, end_date: Syste /// 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) +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(); diff --git a/src/models.rs b/src/models.rs index b96c8d2..4e2ca03 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,5 +1,4 @@ -use std::time::SystemTime; -use time::Date; +use chrono::{NaiveDate, NaiveDateTime}; use serde::{Deserialize, Serialize}; use cfg_if::cfg_if; @@ -39,8 +38,8 @@ pub struct User { #[cfg_attr(feature = "ssr", diesel(deserialize_as = String))] pub password: Option, /// The time the user was created - #[cfg_attr(feature = "ssr", diesel(deserialize_as = SystemTime))] - pub created_at: Option, + #[cfg_attr(feature = "ssr", diesel(deserialize_as = NaiveDateTime))] + pub created_at: Option, /// Whether the user is an admin pub admin: bool, } @@ -103,7 +102,7 @@ impl User { /// #[cfg(feature = "ssr")] pub fn get_history_songs(self: &Self, limit: Option, conn: &mut PgPooledConn) -> - Result, Box> { + Result, Box> { use crate::schema::songs::dsl::*; use crate::schema::song_history::dsl::*; @@ -467,7 +466,7 @@ pub struct Album { /// The album's title pub title: String, /// The album's release date - pub release_date: Option, + pub release_date: Option, /// The path to the album's image file pub image_path: Option, } @@ -546,7 +545,7 @@ pub struct Song { /// The duration of the song in seconds pub duration: i32, /// The song's release date - pub release_date: Option, + pub release_date: Option, /// The path to the song's audio file pub storage_path: String, /// The path to the song's image file @@ -622,7 +621,7 @@ pub struct HistoryEntry { /// The id of the user who listened to the song pub user_id: i32, /// The date the song was listened to - pub date: SystemTime, + pub date: NaiveDateTime, /// The id of the song that was listened to pub song_id: i32, } diff --git a/src/pages/profile.rs b/src/pages/profile.rs index 8bec37a..744229f 100644 --- a/src/pages/profile.rs +++ b/src/pages/profile.rs @@ -1,5 +1,4 @@ use leptos::*; -use leptos::logging::*; use leptos_router::use_params_map; use leptos_icons::*; use server_fn::error::NoCustomError; @@ -17,7 +16,7 @@ use crate::models::User; use crate::users::get_user_by_id; /// Duration in seconds backwards from now to aggregate history data for -const HISTORY_SECS: u64 = 60 * 60 * 24 * 30; +const HISTORY_SECS: i64 = 60 * 60 * 24 * 30; const HISTORY_MESSAGE: &str = "Last Month"; /// How many top songs to show @@ -160,19 +159,7 @@ fn UserProfile(user: User) -> impl IntoView { {user.email} { user.created_at.map(|created_at| { - use time::{OffsetDateTime, macros::format_description}; - let format = format_description!("[month repr:long] [year]"); - let date_time = Into::::into(created_at).format(format); - - match date_time { - Ok(date_time) => { - format!(" • Joined {}", date_time) - }, - Err(e) => { - error!("Error formatting date: {}", e); - String::new() - } - } + format!(" • Joined {}", created_at.format("%B %Y")) }) } { @@ -191,11 +178,10 @@ fn UserProfile(user: User) -> impl IntoView { #[component] fn TopSongs(#[prop(into)] user_id: MaybeSignal) -> impl IntoView { let top_songs = create_resource(move || user_id.get(), |user_id| async move { - use std::time::{SystemTime, Duration}; - - let now = SystemTime::now(); - let start = now - Duration::from_secs(HISTORY_SECS); - let top_songs = top_songs(user_id, start, now, Some(TOP_SONGS_COUNT)).await; + use chrono::{Local, Duration}; + let now = Local::now(); + let start = now - Duration::seconds(HISTORY_SECS); + let top_songs = top_songs(user_id, start.naive_utc(), now.naive_utc(), Some(TOP_SONGS_COUNT)).await; top_songs.map(|top_songs| { top_songs.into_iter().map(|(plays, song)| { @@ -283,11 +269,11 @@ fn RecentSongs(#[prop(into)] user_id: MaybeSignal) -> impl IntoView { #[component] fn TopArtists(#[prop(into)] user_id: MaybeSignal) -> impl IntoView { let top_artists = create_resource(move || user_id.get(), |user_id| async move { - use std::time::{SystemTime, Duration}; + use chrono::{Local, Duration}; - let now = SystemTime::now(); - let start = now - Duration::from_secs(HISTORY_SECS); - let top_artists = top_artists(user_id, start, now, Some(TOP_ARTISTS_COUNT)).await; + 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)| { diff --git a/src/songdata.rs b/src/songdata.rs index bbbb64b..a70707f 100644 --- a/src/songdata.rs +++ b/src/songdata.rs @@ -2,7 +2,7 @@ use crate::models::{Album, Artist, Song}; use crate::components::dashboard_tile::DashboardTile; use serde::{Serialize, Deserialize}; -use time::Date; +use chrono::NaiveDate; /// Holds information about a song /// @@ -22,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, 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));