Switch to chrono instead of time

This commit is contained in:
Ethan Girouard 2024-11-04 18:23:25 -05:00
parent 820a1d816a
commit 6dcbba2588
Signed by: eta357
GPG Key ID: 7BCDC36DFD11C146
9 changed files with 43 additions and 56 deletions

11
Cargo.lock generated
View File

@ -378,13 +378,16 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.37" version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [ dependencies = [
"android-tzdata", "android-tzdata",
"iana-time-zone", "iana-time-zone",
"js-sys",
"num-traits", "num-traits",
"serde",
"wasm-bindgen",
"windows-targets 0.52.4", "windows-targets 0.52.4",
] ]
@ -690,11 +693,11 @@ checksum = "03fc05c17098f21b89bc7d98fe1dd3cce2c11c2ad8e145f2a44fe08ed28eb559"
dependencies = [ dependencies = [
"bitflags 2.5.0", "bitflags 2.5.0",
"byteorder", "byteorder",
"chrono",
"diesel_derives", "diesel_derives",
"itoa", "itoa",
"pq-sys", "pq-sys",
"r2d2", "r2d2",
"time",
] ]
[[package]] [[package]]
@ -1834,6 +1837,7 @@ dependencies = [
"axum", "axum",
"axum-login", "axum-login",
"cfg-if", "cfg-if",
"chrono",
"console_error_panic_hook", "console_error_panic_hook",
"diesel", "diesel",
"diesel_migrations", "diesel_migrations",
@ -1857,7 +1861,6 @@ dependencies = [
"server_fn", "server_fn",
"symphonia", "symphonia",
"thiserror", "thiserror",
"time",
"tokio", "tokio",
"tower 0.5.1", "tower 0.5.1",
"tower-http", "tower-http",

View File

@ -19,11 +19,10 @@ wasm-bindgen = { version = "=0.2.93", default-features = false, optional = true
leptos_icons = { version = "0.3.0" } leptos_icons = { version = "0.3.0" }
icondata = { version = "0.3.0" } icondata = { version = "0.3.0" }
dotenv = { version = "0.15.0", optional = true } 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 } lazy_static = { version = "1.4.0", optional = true }
serde = { version = "1.0.195", features = ["derive"], default-features = false } serde = { version = "1.0.195", features = ["derive"], default-features = false }
openssl = { version = "0.10.63", optional = true } 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 } diesel_migrations = { version = "2.1.0", optional = true }
pbkdf2 = { version = "0.12.2", features = ["simple"], optional = true } pbkdf2 = { version = "0.12.2", features = ["simple"], optional = true }
tokio = { version = "1", optional = true, features = ["rt-multi-thread"] } 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" web-sys = "0.3.69"
leptos-use = "0.13.5" leptos-use = "0.13.5"
image-convert = { version = "0.18.0", optional = true, default-features = false } image-convert = { version = "0.18.0", optional = true, default-features = false }
chrono = { version = "0.4.38", default-features = false, features = ["serde", "clock"] }
[features] [features]
hydrate = [ hydrate = [
@ -50,6 +50,7 @@ hydrate = [
"leptos_router/hydrate", "leptos_router/hydrate",
"console_error_panic_hook", "console_error_panic_hook",
"wasm-bindgen", "wasm-bindgen",
"chrono/wasmbind",
] ]
ssr = [ ssr = [
"dep:leptos_axum", "dep:leptos_axum",

View File

@ -1,7 +1,7 @@
use crate::models::Artist; use crate::models::Artist;
use crate::components::dashboard_tile::DashboardTile; use crate::components::dashboard_tile::DashboardTile;
use time::Date; use chrono::NaiveDate;
/// Holds information about an album /// Holds information about an album
/// ///
@ -14,7 +14,7 @@ pub struct AlbumData {
/// Album artists /// Album artists
pub artists: Vec<Artist>, pub artists: Vec<Artist>,
/// Album release date /// Album release date
pub release_date: Option<Date>, pub release_date: Option<NaiveDate>,
/// Path to album image, relative to the root of the web server. /// Path to album image, relative to the root of the web server.
/// For example, `"/assets/images/Album.jpg"` /// For example, `"/assets/images/Album.jpg"`
pub image_path: String, pub image_path: String,

View File

@ -1,4 +1,4 @@
use std::time::SystemTime; use chrono::NaiveDateTime;
use leptos::*; use leptos::*;
use crate::models::HistoryEntry; use crate::models::HistoryEntry;
use crate::models::Song; use crate::models::Song;
@ -25,7 +25,7 @@ pub async fn get_history(limit: Option<i64>) -> Result<Vec<HistoryEntry>, Server
/// Get the listen dates and songs of the current user. /// Get the listen dates and songs of the current user.
#[server(endpoint = "history/get_songs")] #[server(endpoint = "history/get_songs")]
pub async fn get_history_songs(limit: Option<i64>) -> Result<Vec<(SystemTime, Song)>, ServerFnError> { pub async fn get_history_songs(limit: Option<i64>) -> Result<Vec<(NaiveDateTime, Song)>, ServerFnError> {
let user = get_user().await?; let user = get_user().await?;
let db_con = &mut get_db_conn(); let db_con = &mut get_db_conn();
let songs = user.get_history_songs(limit, db_con) let songs = user.get_history_songs(limit, db_con)

View File

@ -6,7 +6,7 @@ use cfg_if::cfg_if;
use crate::songdata::SongData; use crate::songdata::SongData;
use crate::artistdata::ArtistData; use crate::artistdata::ArtistData;
use std::time::SystemTime; use chrono::NaiveDateTime;
cfg_if! { cfg_if! {
if #[cfg(feature = "ssr")] { 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 /// Returns a list of tuples with the date the song was listened to
/// and the song data, sorted by date (most recent first). /// and the song data, sorted by date (most recent first).
#[server(endpoint = "/profile/recent_songs")] #[server(endpoint = "/profile/recent_songs")]
pub async fn recent_songs(for_user_id: i32, limit: Option<i64>) -> Result<Vec<(SystemTime, SongData)>, ServerFnError> { pub async fn recent_songs(for_user_id: i32, limit: Option<i64>) -> Result<Vec<(NaiveDateTime, SongData)>, ServerFnError> {
let mut db_con = get_db_conn(); let mut db_con = get_db_conn();
// Get the ids of the most recent songs listened to // 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<i64>) -> Result<Vec<(S
.load(&mut db_con)?; .load(&mut db_con)?;
// Process the history data into a map of song ids to song data // 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()); let mut history_songs: HashMap<i32, (NaiveDateTime, SongData)> = HashMap::with_capacity(history.len());
for (history, song, album, artist, like, dislike) in history { for (history, song, album, artist, like, dislike) in history {
let song_id = history.song_id; let song_id = history.song_id;
@ -148,7 +148,7 @@ pub async fn recent_songs(for_user_id: i32, limit: Option<i64>) -> Result<Vec<(S
} }
// Sort the songs by date // Sort the songs by date
let mut history_songs: Vec<(SystemTime, SongData)> = 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)); history_songs.sort_by(|a, b| b.0.cmp(&a.0));
Ok(history_songs) Ok(history_songs)
} }
@ -158,7 +158,7 @@ pub async fn recent_songs(for_user_id: i32, limit: Option<i64>) -> Result<Vec<(S
/// If not provided, all songs listened to in the date range are 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). /// 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")] #[server(endpoint = "/profile/top_songs")]
pub async fn top_songs(for_user_id: i32, start_date: SystemTime, end_date: SystemTime, limit: Option<i64>) pub async fn top_songs(for_user_id: i32, start_date: NaiveDateTime, end_date: NaiveDateTime, limit: Option<i64>)
-> Result<Vec<(i64, SongData)>, ServerFnError> -> Result<Vec<(i64, SongData)>, ServerFnError>
{ {
let mut db_con = get_db_conn(); 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. /// 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). /// 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")] #[server(endpoint = "/profile/top_artists")]
pub async fn top_artists(for_user_id: i32, start_date: SystemTime, end_date: SystemTime, limit: Option<i64>) pub async fn top_artists(for_user_id: i32, start_date: NaiveDateTime, end_date: NaiveDateTime, limit: Option<i64>)
-> Result<Vec<(i64, ArtistData)>, ServerFnError> -> Result<Vec<(i64, ArtistData)>, ServerFnError>
{ {
let mut db_con = get_db_conn(); let mut db_con = get_db_conn();

View File

@ -1,5 +1,4 @@
use std::time::SystemTime; use chrono::{NaiveDate, NaiveDateTime};
use time::Date;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use cfg_if::cfg_if; use cfg_if::cfg_if;
@ -39,8 +38,8 @@ pub struct User {
#[cfg_attr(feature = "ssr", diesel(deserialize_as = String))] #[cfg_attr(feature = "ssr", diesel(deserialize_as = String))]
pub password: Option<String>, pub password: Option<String>,
/// 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 = NaiveDateTime))]
pub created_at: Option<SystemTime>, pub created_at: Option<NaiveDateTime>,
/// Whether the user is an admin /// Whether the user is an admin
pub admin: bool, pub admin: bool,
} }
@ -103,7 +102,7 @@ impl User {
/// ///
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
pub fn get_history_songs(self: &Self, limit: Option<i64>, conn: &mut PgPooledConn) -> pub fn get_history_songs(self: &Self, limit: Option<i64>, conn: &mut PgPooledConn) ->
Result<Vec<(SystemTime, Song)>, Box<dyn Error>> { Result<Vec<(NaiveDateTime, Song)>, Box<dyn Error>> {
use crate::schema::songs::dsl::*; use crate::schema::songs::dsl::*;
use crate::schema::song_history::dsl::*; use crate::schema::song_history::dsl::*;
@ -467,7 +466,7 @@ pub struct Album {
/// The album's title /// The album's title
pub title: String, pub title: String,
/// The album's release date /// The album's release date
pub release_date: Option<Date>, pub release_date: Option<NaiveDate>,
/// The path to the album's image file /// The path to the album's image file
pub image_path: Option<String>, pub image_path: Option<String>,
} }
@ -546,7 +545,7 @@ pub struct Song {
/// The duration of the song in seconds /// The duration of the song in seconds
pub duration: i32, pub duration: i32,
/// The song's release date /// The song's release date
pub release_date: Option<Date>, pub release_date: Option<NaiveDate>,
/// The path to the song's audio file /// The path to the song's audio file
pub storage_path: String, pub storage_path: String,
/// The path to the song's image file /// 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 /// The id of the user who listened to the song
pub user_id: i32, pub user_id: i32,
/// The date the song was listened to /// The date the song was listened to
pub date: SystemTime, pub date: NaiveDateTime,
/// The id of the song that was listened to /// The id of the song that was listened to
pub song_id: i32, pub song_id: i32,
} }

View File

@ -1,5 +1,4 @@
use leptos::*; use leptos::*;
use leptos::logging::*;
use leptos_router::use_params_map; use leptos_router::use_params_map;
use leptos_icons::*; use leptos_icons::*;
use server_fn::error::NoCustomError; use server_fn::error::NoCustomError;
@ -17,7 +16,7 @@ use crate::models::User;
use crate::users::get_user_by_id; use crate::users::get_user_by_id;
/// Duration in seconds backwards from now to aggregate history data for /// 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"; const HISTORY_MESSAGE: &str = "Last Month";
/// How many top songs to show /// How many top songs to show
@ -160,19 +159,7 @@ fn UserProfile(user: User) -> impl IntoView {
{user.email} {user.email}
{ {
user.created_at.map(|created_at| { user.created_at.map(|created_at| {
use time::{OffsetDateTime, macros::format_description}; format!(" • Joined {}", created_at.format("%B %Y"))
let format = format_description!("[month repr:long] [year]");
let date_time = Into::<OffsetDateTime>::into(created_at).format(format);
match date_time {
Ok(date_time) => {
format!(" • Joined {}", date_time)
},
Err(e) => {
error!("Error formatting date: {}", e);
String::new()
}
}
}) })
} }
{ {
@ -191,11 +178,10 @@ fn UserProfile(user: User) -> impl IntoView {
#[component] #[component]
fn TopSongs(#[prop(into)] user_id: MaybeSignal<i32>) -> impl IntoView { fn TopSongs(#[prop(into)] user_id: MaybeSignal<i32>) -> impl IntoView {
let top_songs = create_resource(move || user_id.get(), |user_id| async move { let top_songs = create_resource(move || user_id.get(), |user_id| async move {
use std::time::{SystemTime, Duration}; use chrono::{Local, Duration};
let now = Local::now();
let now = SystemTime::now(); let start = now - Duration::seconds(HISTORY_SECS);
let start = now - Duration::from_secs(HISTORY_SECS); let top_songs = top_songs(user_id, start.naive_utc(), now.naive_utc(), Some(TOP_SONGS_COUNT)).await;
let top_songs = top_songs(user_id, start, now, Some(TOP_SONGS_COUNT)).await;
top_songs.map(|top_songs| { top_songs.map(|top_songs| {
top_songs.into_iter().map(|(plays, song)| { top_songs.into_iter().map(|(plays, song)| {
@ -283,11 +269,11 @@ fn RecentSongs(#[prop(into)] user_id: MaybeSignal<i32>) -> impl IntoView {
#[component] #[component]
fn TopArtists(#[prop(into)] user_id: MaybeSignal<i32>) -> impl IntoView { fn TopArtists(#[prop(into)] user_id: MaybeSignal<i32>) -> impl IntoView {
let top_artists = create_resource(move || user_id.get(), |user_id| async move { 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 now = Local::now();
let start = now - Duration::from_secs(HISTORY_SECS); let start = now - Duration::seconds(HISTORY_SECS);
let top_artists = top_artists(user_id, start, now, Some(TOP_ARTISTS_COUNT)).await; 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.map(|top_artists| {
top_artists.into_iter().map(|(_plays, artist)| { top_artists.into_iter().map(|(_plays, artist)| {

View File

@ -2,7 +2,7 @@ use crate::models::{Album, Artist, Song};
use crate::components::dashboard_tile::DashboardTile; use crate::components::dashboard_tile::DashboardTile;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use time::Date; use chrono::NaiveDate;
/// Holds information about a song /// Holds information about a song
/// ///
@ -22,7 +22,7 @@ pub struct SongData {
/// The duration of the song in seconds /// The duration of the song in seconds
pub duration: i32, pub duration: i32,
/// The song's release date /// The song's release date
pub release_date: Option<Date>, pub release_date: Option<NaiveDate>,
/// 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,

View File

@ -10,7 +10,7 @@ cfg_if! {
use diesel::prelude::*; use diesel::prelude::*;
use log::*; use log::*;
use server_fn::error::NoCustomError; 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<Option<i3
/// Validate the release date in a multipart field /// Validate the release date in a multipart field
/// Expects a field with a release date, and ensures it is a valid date in the format [year]-[month]-[day] /// Expects a field with a release date, and ensures it is a valid date in the format [year]-[month]-[day]
#[cfg(feature = "ssr")] #[cfg(feature = "ssr")]
async fn validate_release_date(release_date: Field<'static>) -> Result<Option<Date>, ServerFnError> { async fn validate_release_date(release_date: Field<'static>) -> Result<Option<NaiveDate>, ServerFnError> {
match release_date.text().await { match release_date.text().await {
Ok(release_date) => { Ok(release_date) => {
if release_date.trim().is_empty() { if release_date.trim().is_empty() {
return Ok(None); return Ok(None);
} }
let date_format = time::macros::format_description!("[year]-[month]-[day]"); let release_date = NaiveDate::parse_from_str(&release_date.trim(), "%Y-%m-%d");
let release_date = Date::parse(&release_date.trim(), date_format);
match release_date { match release_date {
Ok(release_date) => Ok(Some(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()))?; ServerError("Title field required and must precede file field".to_string()))?;
let clean_title = title.replace(" ", "_").replace("/", "_"); let clean_title = title.replace(" ", "_").replace("/", "_");
let date_format = time::macros::format_description!("[year]-[month]-[day]_[hour]:[minute]:[second]"); let date_str = chrono::Utc::now().format("%Y-%m-%d_%H:%M:%S").to_string();
let date_str = time::OffsetDateTime::now_utc().format(date_format).unwrap_or_default();
let upload_path = format!("assets/audio/upload-{}_{}.mp3", date_str, clean_title); let upload_path = format!("assets/audio/upload-{}_{}.mp3", date_str, clean_title);
file_name = Some(format!("upload-{}_{}.mp3", date_str, clean_title)); file_name = Some(format!("upload-{}_{}.mp3", date_str, clean_title));