Merge pull request 'Fix time not implemented on wasm on profile page' (#135) from 134-fix-time-not-implemented-on-wasm into main

Reviewed-on: LibreTunes/LibreTunes#135
This commit is contained in:
Ethan Girouard 2024-11-04 23:40:11 +00:00
commit b800e4ee47
9 changed files with 43 additions and 56 deletions

11
Cargo.lock generated
View File

@ -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",

View File

@ -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",

View File

@ -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<Artist>,
/// 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.
/// For example, `"/assets/images/Album.jpg"`
pub image_path: String,

View File

@ -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<i64>) -> Result<Vec<HistoryEntry>, Server
/// Get the listen dates and songs of the current user.
#[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 db_con = &mut get_db_conn();
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::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<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();
// 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)?;
// 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 {
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
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));
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.
/// 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>)
pub async fn top_songs(for_user_id: i32, start_date: NaiveDateTime, end_date: NaiveDateTime, limit: Option<i64>)
-> Result<Vec<(i64, SongData)>, 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<i64>)
pub async fn top_artists(for_user_id: i32, start_date: NaiveDateTime, end_date: NaiveDateTime, limit: Option<i64>)
-> Result<Vec<(i64, ArtistData)>, ServerFnError>
{
let mut db_con = get_db_conn();

View File

@ -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<String>,
/// The time the user was created
#[cfg_attr(feature = "ssr", diesel(deserialize_as = SystemTime))]
pub created_at: Option<SystemTime>,
#[cfg_attr(feature = "ssr", diesel(deserialize_as = NaiveDateTime))]
pub created_at: Option<NaiveDateTime>,
/// 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<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::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<Date>,
pub release_date: Option<NaiveDate>,
/// The path to the album's image file
pub image_path: Option<String>,
}
@ -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<Date>,
pub release_date: Option<NaiveDate>,
/// 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,
}

View File

@ -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::<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()
}
}
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<i32>) -> 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<i32>) -> impl IntoView {
#[component]
fn TopArtists(#[prop(into)] user_id: MaybeSignal<i32>) -> 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)| {

View File

@ -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<Date>,
pub release_date: Option<NaiveDate>,
/// Path to song file, relative to the root of the web server.
/// For example, `"/assets/audio/Song.mp3"`
pub song_path: String,

View File

@ -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<Option<i3
/// 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]
#[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 {
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));