From 7280ed1587ffd7efd4c7b6166c848c288a37a2c3 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 11 Dec 2024 23:24:17 +0530 Subject: [PATCH 01/17] feat(backend): start implementation for hevy import --- crates/enums/src/lib.rs | 17 +- crates/services/importer/src/hevy.rs | 218 ++++++++++++++++++ crates/services/importer/src/lib.rs | 2 + libs/generated/src/graphql/backend/graphql.ts | 1 + .../src/graphql/backend/types.generated.ts | 1 + 5 files changed, 231 insertions(+), 8 deletions(-) create mode 100644 crates/services/importer/src/hevy.rs diff --git a/crates/enums/src/lib.rs b/crates/enums/src/lib.rs index 12a333f2e1..0c9108f032 100644 --- a/crates/enums/src/lib.rs +++ b/crates/enums/src/lib.rs @@ -200,6 +200,7 @@ pub enum ImportSource { Igdb, Imdb, Plex, + Hevy, Trakt, Movary, Jellyfin, @@ -565,17 +566,17 @@ pub enum IntegrationProvider { } #[derive( - Debug, - Clone, - Copy, - PartialEq, Eq, - EnumIter, - DeriveActiveEnum, - Deserialize, - Serialize, Enum, + Copy, + Clone, + Debug, Display, + EnumIter, + PartialEq, + Serialize, + Deserialize, + DeriveActiveEnum, )] #[sea_orm( rs_type = "String", diff --git a/crates/services/importer/src/hevy.rs b/crates/services/importer/src/hevy.rs new file mode 100644 index 0000000000..bf715c6ac7 --- /dev/null +++ b/crates/services/importer/src/hevy.rs @@ -0,0 +1,218 @@ +use std::{collections::HashMap, fs, sync::Arc}; + +use async_graphql::Result; +use chrono::{Duration, NaiveDateTime}; +use common_utils::ryot_log; +use csv::ReaderBuilder; +use database_models::{exercise, prelude::Exercise}; +use dependent_models::{ImportCompletedItem, ImportResult}; +use dependent_utils::generate_exercise_id; +use enums::{ExerciseLot, ExerciseSource}; +use fitness_models::{ + SetLot, UserExerciseInput, UserWorkoutInput, UserWorkoutSetRecord, WorkoutSetStatistic, +}; +use importer_models::{ImportFailStep, ImportFailedItem}; +use indexmap::IndexMap; +use itertools::Itertools; +use media_models::DeployGenericCsvImportInput; +use rust_decimal::Decimal; +use rust_decimal_macros::dec; +use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; +use serde::{Deserialize, Serialize}; +use supporting_service::SupportingService; + +use super::utils; + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +#[serde(rename_all = "PascalCase")] +struct Entry { + #[serde(alias = "Workout #")] + workout_number: String, + date: String, + #[serde(alias = "Workout Name")] + workout_name: String, + #[serde(alias = "Duration (sec)", alias = "Duration")] + workout_duration: String, + #[serde(alias = "Exercise Name")] + exercise_name: String, + #[serde(alias = "Set Order")] + set_order: String, + #[serde(alias = "Weight (kg)")] + weight: Option, + reps: Option, + #[serde(alias = "Distance (m)")] + distance: Option, + seconds: Option, + notes: Option, + #[serde(alias = "Workout Notes")] + workout_notes: Option, +} + +pub async fn import( + input: DeployGenericCsvImportInput, + ss: &Arc, + user_id: &str, +) -> Result { + let mut completed = vec![]; + let mut failed = vec![]; + let file_string = fs::read_to_string(&input.csv_path)?; + // DEV: Delimiter is `;` on android and `,` on iOS, so we determine it by reading the first line + let data = file_string.clone(); + let first_line = data.lines().next().unwrap(); + let delimiter = if first_line.contains(';') { + b';' + } else if first_line.contains(',') { + b',' + } else { + return Err("Could not determine delimiter".into()); + }; + + let mut unique_exercises: HashMap = HashMap::new(); + let entries_reader = ReaderBuilder::new() + .delimiter(delimiter) + .from_reader(file_string.as_bytes()) + .deserialize::() + .map(|r| r.unwrap()) + .collect_vec(); + + let mut workouts_to_entries = IndexMap::new(); + for entry in entries_reader.clone() { + workouts_to_entries + .entry(entry.workout_number.clone()) + .or_insert(vec![]) + .push(entry); + } + + let mut exercises_to_workouts = IndexMap::new(); + + for (workout_number, entries) in workouts_to_entries { + let mut exercises = IndexMap::new(); + for entry in entries { + exercises + .entry(entry.exercise_name.clone()) + .or_insert(vec![]) + .push(entry); + } + exercises_to_workouts.insert(workout_number, exercises); + } + + for (_workout_number, workout) in exercises_to_workouts { + let first_exercise = workout.first().unwrap().1.first().unwrap(); + let ndt = NaiveDateTime::parse_from_str(&first_exercise.date, "%Y-%m-%d %H:%M:%S") + .expect("Failed to parse input string"); + let ndt = utils::get_date_time_with_offset(ndt, &ss.timezone); + let workout_duration = + Duration::try_seconds(first_exercise.workout_duration.parse().unwrap()).unwrap(); + let mut collected_exercises = vec![]; + for (exercise_name, exercises) in workout.clone() { + let mut collected_sets = vec![]; + let mut notes = vec![]; + let valid_ex = exercises.iter().find(|e| e.set_order != "Note").unwrap(); + let exercise_lot = if valid_ex.seconds.is_some() && valid_ex.distance.is_some() { + ExerciseLot::DistanceAndDuration + } else if valid_ex.seconds.is_some() { + ExerciseLot::Duration + } else if valid_ex.reps.is_some() && valid_ex.weight.is_some() { + ExerciseLot::RepsAndWeight + } else if valid_ex.reps.is_some() { + ExerciseLot::Reps + } else { + failed.push(ImportFailedItem { + lot: None, + step: ImportFailStep::InputTransformation, + identifier: format!( + "Workout #{}, Set #{}", + valid_ex.workout_number, valid_ex.set_order + ), + error: Some(format!( + "Could not determine exercise lot: {}", + serde_json::to_string(&valid_ex).unwrap() + )), + }); + continue; + }; + let existing_exercise = Exercise::find() + .filter(exercise::Column::Lot.eq(exercise_lot)) + .filter(exercise::Column::Name.eq(&exercise_name)) + .one(&ss.db) + .await?; + let generated_id = generate_exercise_id(&exercise_name, exercise_lot, user_id); + let exercise_id = match existing_exercise { + Some(db_ex) + if db_ex.source == ExerciseSource::Github || db_ex.id == generated_id => + { + db_ex.id + } + _ => match unique_exercises.get(&exercise_name) { + Some(mem_ex) => mem_ex.id.clone(), + None => { + unique_exercises.insert( + exercise_name.clone(), + exercise::Model { + lot: exercise_lot, + name: exercise_name, + id: generated_id.clone(), + ..Default::default() + }, + ); + generated_id + } + }, + }; + ryot_log!(debug, "Importing exercise with id = {}", exercise_id); + for set in exercises { + if let Some(note) = set.notes { + notes.push(note); + } + let weight = set.weight.map(|d| if d == dec!(0) { dec!(1) } else { d }); + let set_lot = match set.set_order.as_str() { + "W" => SetLot::WarmUp, + "F" => SetLot::Failure, + "D" => SetLot::Drop, + _ => SetLot::Normal, + }; + collected_sets.push(UserWorkoutSetRecord { + statistic: WorkoutSetStatistic { + weight, + reps: set.reps, + duration: set.seconds.and_then(|r| r.checked_div(dec!(60))), + distance: set.distance.and_then(|d| d.checked_div(dec!(1000))), + ..Default::default() + }, + rpe: None, + note: None, + lot: set_lot, + rest_time: None, + confirmed_at: None, + }); + } + collected_exercises.push(UserExerciseInput { + notes, + exercise_id, + assets: None, + sets: collected_sets, + }); + } + completed.push(ImportCompletedItem::Workout(UserWorkoutInput { + assets: None, + start_time: ndt, + supersets: vec![], + template_id: None, + repeated_from: None, + create_workout_id: None, + update_workout_id: None, + exercises: collected_exercises, + end_time: ndt + workout_duration, + update_workout_template_id: None, + name: first_exercise.workout_name.clone(), + comment: first_exercise.workout_notes.clone(), + })); + } + completed.extend( + unique_exercises + .values() + .cloned() + .map(ImportCompletedItem::Exercise), + ); + Ok(ImportResult { failed, completed }) +} diff --git a/crates/services/importer/src/lib.rs b/crates/services/importer/src/lib.rs index 2f4d7d1878..4dc2666432 100644 --- a/crates/services/importer/src/lib.rs +++ b/crates/services/importer/src/lib.rs @@ -24,6 +24,7 @@ use traits::TraceOk; mod audiobookshelf; mod generic_json; mod goodreads; +mod hevy; mod igdb; mod imdb; mod jellyfin; @@ -78,6 +79,7 @@ impl ImporterService { ImportSource::StrongApp => { strong_app::import(input.strong_app.unwrap(), &self.0, &user_id).await } + ImportSource::Hevy => hevy::import(input.generic_csv.unwrap(), &self.0, &user_id).await, ImportSource::Mediatracker => mediatracker::import(input.url_and_key.unwrap()).await, ImportSource::Myanimelist => myanimelist::import(input.mal.unwrap()).await, ImportSource::Goodreads => { diff --git a/libs/generated/src/graphql/backend/graphql.ts b/libs/generated/src/graphql/backend/graphql.ts index 2ffc0de62e..5a72a4b41c 100644 --- a/libs/generated/src/graphql/backend/graphql.ts +++ b/libs/generated/src/graphql/backend/graphql.ts @@ -869,6 +869,7 @@ export enum ImportSource { Audiobookshelf = 'AUDIOBOOKSHELF', GenericJson = 'GENERIC_JSON', Goodreads = 'GOODREADS', + Hevy = 'HEVY', Igdb = 'IGDB', Imdb = 'IMDB', Jellyfin = 'JELLYFIN', diff --git a/libs/generated/src/graphql/backend/types.generated.ts b/libs/generated/src/graphql/backend/types.generated.ts index e3aef8ad43..3efe9d0b3f 100644 --- a/libs/generated/src/graphql/backend/types.generated.ts +++ b/libs/generated/src/graphql/backend/types.generated.ts @@ -892,6 +892,7 @@ export enum ImportSource { Audiobookshelf = 'AUDIOBOOKSHELF', GenericJson = 'GENERIC_JSON', Goodreads = 'GOODREADS', + Hevy = 'HEVY', Igdb = 'IGDB', Imdb = 'IMDB', Jellyfin = 'JELLYFIN', From a1a7d1ea16a79ea7fa89bc431f18eb043e9b2e76 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 11 Dec 2024 23:26:18 +0530 Subject: [PATCH 02/17] feat(frontend): allow uploading for hevy --- .../_dashboard.settings.imports-and-exports._index.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/frontend/app/routes/_dashboard.settings.imports-and-exports._index.tsx b/apps/frontend/app/routes/_dashboard.settings.imports-and-exports._index.tsx index ddddf926e0..712b6c6ac8 100644 --- a/apps/frontend/app/routes/_dashboard.settings.imports-and-exports._index.tsx +++ b/apps/frontend/app/routes/_dashboard.settings.imports-and-exports._index.tsx @@ -81,10 +81,11 @@ export const action = async ({ request }: ActionFunctionArgs) => { formData.delete("source"); const values = await match(source) .with( - ImportSource.Storygraph, + ImportSource.Hevy, ImportSource.Imdb, - ImportSource.Goodreads, ImportSource.OpenScale, + ImportSource.Goodreads, + ImportSource.Storygraph, () => ({ genericCsv: processSubmission(formData, genericCsvImportFormSchema), }), @@ -270,9 +271,10 @@ export default function Page() { ), ) .with( + ImportSource.Hevy, + ImportSource.Imdb, ImportSource.OpenScale, ImportSource.Goodreads, - ImportSource.Imdb, ImportSource.Storygraph, () => ( <> From 7130331faf7426983ec98659de2ccb8ad9eff11f Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 11 Dec 2024 23:52:28 +0530 Subject: [PATCH 03/17] feat(services/importer): allow importing hevy data --- crates/services/importer/src/hevy.rs | 103 +++++++++++---------------- 1 file changed, 43 insertions(+), 60 deletions(-) diff --git a/crates/services/importer/src/hevy.rs b/crates/services/importer/src/hevy.rs index bf715c6ac7..7d06cb4c3e 100644 --- a/crates/services/importer/src/hevy.rs +++ b/crates/services/importer/src/hevy.rs @@ -1,7 +1,7 @@ use std::{collections::HashMap, fs, sync::Arc}; use async_graphql::Result; -use chrono::{Duration, NaiveDateTime}; +use chrono::NaiveDateTime; use common_utils::ryot_log; use csv::ReaderBuilder; use database_models::{exercise, prelude::Exercise}; @@ -21,31 +21,26 @@ use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; use serde::{Deserialize, Serialize}; use supporting_service::SupportingService; -use super::utils; +use crate::utils; #[derive(Debug, Serialize, Deserialize, Clone, Default)] -#[serde(rename_all = "PascalCase")] struct Entry { - #[serde(alias = "Workout #")] - workout_number: String, - date: String, - #[serde(alias = "Workout Name")] - workout_name: String, - #[serde(alias = "Duration (sec)", alias = "Duration")] - workout_duration: String, - #[serde(alias = "Exercise Name")] - exercise_name: String, - #[serde(alias = "Set Order")] - set_order: String, - #[serde(alias = "Weight (kg)")] - weight: Option, + title: String, + set_index: u8, + rpe: Option, + set_type: String, + end_time: String, + start_time: String, reps: Option, - #[serde(alias = "Distance (m)")] + exercise_title: String, + #[serde(alias = "weight_kg")] + weight: Option, + #[serde(alias = "duration_seconds")] + duration: Option, + #[serde(alias = "distance_km")] distance: Option, - seconds: Option, - notes: Option, - #[serde(alias = "Workout Notes")] - workout_notes: Option, + description: Option, + exercise_notes: Option, } pub async fn import( @@ -56,20 +51,9 @@ pub async fn import( let mut completed = vec![]; let mut failed = vec![]; let file_string = fs::read_to_string(&input.csv_path)?; - // DEV: Delimiter is `;` on android and `,` on iOS, so we determine it by reading the first line - let data = file_string.clone(); - let first_line = data.lines().next().unwrap(); - let delimiter = if first_line.contains(';') { - b';' - } else if first_line.contains(',') { - b',' - } else { - return Err("Could not determine delimiter".into()); - }; - let mut unique_exercises: HashMap = HashMap::new(); let entries_reader = ReaderBuilder::new() - .delimiter(delimiter) + .double_quote(true) .from_reader(file_string.as_bytes()) .deserialize::() .map(|r| r.unwrap()) @@ -78,7 +62,7 @@ pub async fn import( let mut workouts_to_entries = IndexMap::new(); for entry in entries_reader.clone() { workouts_to_entries - .entry(entry.workout_number.clone()) + .entry((entry.start_time.clone(), entry.end_time.clone())) .or_insert(vec![]) .push(entry); } @@ -89,28 +73,22 @@ pub async fn import( let mut exercises = IndexMap::new(); for entry in entries { exercises - .entry(entry.exercise_name.clone()) + .entry(entry.exercise_title.clone()) .or_insert(vec![]) .push(entry); } exercises_to_workouts.insert(workout_number, exercises); } - for (_workout_number, workout) in exercises_to_workouts { + for (workout_identifier, workout) in exercises_to_workouts { let first_exercise = workout.first().unwrap().1.first().unwrap(); - let ndt = NaiveDateTime::parse_from_str(&first_exercise.date, "%Y-%m-%d %H:%M:%S") - .expect("Failed to parse input string"); - let ndt = utils::get_date_time_with_offset(ndt, &ss.timezone); - let workout_duration = - Duration::try_seconds(first_exercise.workout_duration.parse().unwrap()).unwrap(); let mut collected_exercises = vec![]; for (exercise_name, exercises) in workout.clone() { let mut collected_sets = vec![]; - let mut notes = vec![]; - let valid_ex = exercises.iter().find(|e| e.set_order != "Note").unwrap(); - let exercise_lot = if valid_ex.seconds.is_some() && valid_ex.distance.is_some() { + let valid_ex = exercises.first().unwrap(); + let exercise_lot = if valid_ex.duration.is_some() && valid_ex.distance.is_some() { ExerciseLot::DistanceAndDuration - } else if valid_ex.seconds.is_some() { + } else if valid_ex.duration.is_some() { ExerciseLot::Duration } else if valid_ex.reps.is_some() && valid_ex.weight.is_some() { ExerciseLot::RepsAndWeight @@ -121,8 +99,8 @@ pub async fn import( lot: None, step: ImportFailStep::InputTransformation, identifier: format!( - "Workout #{}, Set #{}", - valid_ex.workout_number, valid_ex.set_order + "Workout #{:#?}, Set #{}", + workout_identifier, valid_ex.set_index ), error: Some(format!( "Could not determine exercise lot: {}", @@ -161,21 +139,18 @@ pub async fn import( }; ryot_log!(debug, "Importing exercise with id = {}", exercise_id); for set in exercises { - if let Some(note) = set.notes { - notes.push(note); - } let weight = set.weight.map(|d| if d == dec!(0) { dec!(1) } else { d }); - let set_lot = match set.set_order.as_str() { - "W" => SetLot::WarmUp, - "F" => SetLot::Failure, - "D" => SetLot::Drop, + let set_lot = match set.set_type.as_str() { + "warmup" => SetLot::WarmUp, + "failure" => SetLot::Failure, + "dropset" => SetLot::Drop, _ => SetLot::Normal, }; collected_sets.push(UserWorkoutSetRecord { statistic: WorkoutSetStatistic { weight, reps: set.reps, - duration: set.seconds.and_then(|r| r.checked_div(dec!(60))), + duration: set.duration.and_then(|r| r.checked_div(dec!(60))), distance: set.distance.and_then(|d| d.checked_div(dec!(1000))), ..Default::default() }, @@ -187,25 +162,33 @@ pub async fn import( }); } collected_exercises.push(UserExerciseInput { - notes, exercise_id, assets: None, sets: collected_sets, + notes: first_exercise + .exercise_notes + .clone() + .map(|n| vec![n]) + .unwrap_or_default(), }); } + let start_time = + NaiveDateTime::parse_from_str(&first_exercise.start_time, "%d %b %Y, %H:%M").unwrap(); + let end_time = + NaiveDateTime::parse_from_str(&first_exercise.end_time, "%d %b %Y, %H:%M").unwrap(); completed.push(ImportCompletedItem::Workout(UserWorkoutInput { assets: None, - start_time: ndt, supersets: vec![], template_id: None, repeated_from: None, create_workout_id: None, update_workout_id: None, exercises: collected_exercises, - end_time: ndt + workout_duration, update_workout_template_id: None, - name: first_exercise.workout_name.clone(), - comment: first_exercise.workout_notes.clone(), + name: first_exercise.title.clone(), + comment: first_exercise.description.clone(), + end_time: utils::get_date_time_with_offset(end_time, &ss.timezone), + start_time: utils::get_date_time_with_offset(start_time, &ss.timezone), })); } completed.extend( From 15ddd688974a9e54cea98274c41cb6de39a3dc70 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 11 Dec 2024 23:57:09 +0530 Subject: [PATCH 04/17] chore(services/importer): add support for imperial units --- crates/services/importer/src/hevy.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/services/importer/src/hevy.rs b/crates/services/importer/src/hevy.rs index 7d06cb4c3e..77700558db 100644 --- a/crates/services/importer/src/hevy.rs +++ b/crates/services/importer/src/hevy.rs @@ -33,11 +33,11 @@ struct Entry { start_time: String, reps: Option, exercise_title: String, - #[serde(alias = "weight_kg")] + #[serde(alias = "weight_kg", alias = "weight_lbs")] weight: Option, #[serde(alias = "duration_seconds")] duration: Option, - #[serde(alias = "distance_km")] + #[serde(alias = "distance_km", alias = "distance_miles")] distance: Option, description: Option, exercise_notes: Option, From 042edc89898a9ddcbf97c4bbe3339dbc43dd4557 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Wed, 11 Dec 2024 23:59:03 +0530 Subject: [PATCH 05/17] refactor(services/importer): use inbuilt csv parser --- crates/services/importer/src/hevy.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/services/importer/src/hevy.rs b/crates/services/importer/src/hevy.rs index 77700558db..4420a5a55d 100644 --- a/crates/services/importer/src/hevy.rs +++ b/crates/services/importer/src/hevy.rs @@ -1,9 +1,9 @@ -use std::{collections::HashMap, fs, sync::Arc}; +use std::{collections::HashMap, sync::Arc}; use async_graphql::Result; use chrono::NaiveDateTime; use common_utils::ryot_log; -use csv::ReaderBuilder; +use csv::Reader; use database_models::{exercise, prelude::Exercise}; use dependent_models::{ImportCompletedItem, ImportResult}; use dependent_utils::generate_exercise_id; @@ -50,11 +50,8 @@ pub async fn import( ) -> Result { let mut completed = vec![]; let mut failed = vec![]; - let file_string = fs::read_to_string(&input.csv_path)?; let mut unique_exercises: HashMap = HashMap::new(); - let entries_reader = ReaderBuilder::new() - .double_quote(true) - .from_reader(file_string.as_bytes()) + let entries_reader = Reader::from_path(&input.csv_path)? .deserialize::() .map(|r| r.unwrap()) .collect_vec(); From d40b4940e721920e8a0e115618bd0e9d1a5752c9 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Thu, 12 Dec 2024 00:01:49 +0530 Subject: [PATCH 06/17] docs: add info about heavy imports --- docs/content/importing.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/content/importing.md b/docs/content/importing.md index cb8b9eda2f..473a054a6d 100644 --- a/docs/content/importing.md +++ b/docs/content/importing.md @@ -175,6 +175,19 @@ Exercise" or "Merge Exercise" actions to map the exercise to an existing one. - Scroll down to the "General" section and click on "Export data". - Upload the csv file in the input. +## Hevy + +You can import your workouts from [Hevy](https://www.hevy.com). Exercises will be created +using the same strategy as the [Strong app](#strong-app) importer. + +### Steps + +- Login to your Hevy account on the app and go to the "Profile" page. +- Click on the cog icon on the top right and select "Export & Import Data" under + "Preferences". +- Click on "Export" and then click on the button that says "Export Workouts". +- Upload the csv file in the input. + ## IMDb You can import your watchlist from [IMDb](https://www.imdb.com). They will be added to From b3fdf321fbe48a22a439e86ea931ae36b2bc91c1 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Thu, 12 Dec 2024 00:10:04 +0530 Subject: [PATCH 07/17] refactor(services/importer): use new utility function to associate with exercise --- crates/services/importer/src/hevy.rs | 42 +++++-------------- crates/services/importer/src/lib.rs | 48 ++++++++++++++++++++-- crates/services/importer/src/strong_app.rs | 42 +++++-------------- 3 files changed, 64 insertions(+), 68 deletions(-) diff --git a/crates/services/importer/src/hevy.rs b/crates/services/importer/src/hevy.rs index 4420a5a55d..477a6c8797 100644 --- a/crates/services/importer/src/hevy.rs +++ b/crates/services/importer/src/hevy.rs @@ -4,10 +4,9 @@ use async_graphql::Result; use chrono::NaiveDateTime; use common_utils::ryot_log; use csv::Reader; -use database_models::{exercise, prelude::Exercise}; +use database_models::exercise; use dependent_models::{ImportCompletedItem, ImportResult}; -use dependent_utils::generate_exercise_id; -use enums::{ExerciseLot, ExerciseSource}; +use enums::ExerciseLot; use fitness_models::{ SetLot, UserExerciseInput, UserWorkoutInput, UserWorkoutSetRecord, WorkoutSetStatistic, }; @@ -17,7 +16,6 @@ use itertools::Itertools; use media_models::DeployGenericCsvImportInput; use rust_decimal::Decimal; use rust_decimal_macros::dec; -use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; use serde::{Deserialize, Serialize}; use supporting_service::SupportingService; @@ -106,34 +104,14 @@ pub async fn import( }); continue; }; - let existing_exercise = Exercise::find() - .filter(exercise::Column::Lot.eq(exercise_lot)) - .filter(exercise::Column::Name.eq(&exercise_name)) - .one(&ss.db) - .await?; - let generated_id = generate_exercise_id(&exercise_name, exercise_lot, user_id); - let exercise_id = match existing_exercise { - Some(db_ex) - if db_ex.source == ExerciseSource::Github || db_ex.id == generated_id => - { - db_ex.id - } - _ => match unique_exercises.get(&exercise_name) { - Some(mem_ex) => mem_ex.id.clone(), - None => { - unique_exercises.insert( - exercise_name.clone(), - exercise::Model { - lot: exercise_lot, - name: exercise_name, - id: generated_id.clone(), - ..Default::default() - }, - ); - generated_id - } - }, - }; + let exercise_id = utils::associate_with_existing_or_new_exercise( + user_id, + &exercise_name, + exercise_lot, + ss, + &mut unique_exercises, + ) + .await?; ryot_log!(debug, "Importing exercise with id = {}", exercise_id); for set in exercises { let weight = set.weight.map(|d| if d == dec!(0) { dec!(1) } else { d }); diff --git a/crates/services/importer/src/lib.rs b/crates/services/importer/src/lib.rs index 4dc2666432..87972744a9 100644 --- a/crates/services/importer/src/lib.rs +++ b/crates/services/importer/src/lib.rs @@ -1,15 +1,19 @@ -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; use async_graphql::Result; use background::ApplicationJob; use chrono::{DateTime, Duration, NaiveDateTime, Offset, TimeZone, Utc}; use common_models::BackgroundJob; use common_utils::ryot_log; -use database_models::{import_report, prelude::ImportReport}; +use database_models::{ + exercise, import_report, + prelude::{Exercise, ImportReport}, +}; use dependent_utils::{ - commit_metadata, deploy_background_job, get_google_books_service, get_openlibrary_service, - get_tmdb_non_media_service, process_import, + commit_metadata, deploy_background_job, generate_exercise_id, get_google_books_service, + get_openlibrary_service, get_tmdb_non_media_service, process_import, }; +use enums::{ExerciseLot, ExerciseSource}; use enums::{ImportSource, MediaSource}; use importer_models::{ImportFailStep, ImportFailedItem}; use media_models::{DeployImportJobInput, ImportOrExportMetadataItem}; @@ -200,4 +204,40 @@ pub mod utils { } identifier.map(|id| (id, source)) } + + pub async fn associate_with_existing_or_new_exercise( + user_id: &str, + exercise_name: &String, + exercise_lot: ExerciseLot, + ss: &Arc, + unique_exercises: &mut HashMap, + ) -> Result { + let existing_exercise = Exercise::find() + .filter(exercise::Column::Lot.eq(exercise_lot)) + .filter(exercise::Column::Name.eq(exercise_name)) + .one(&ss.db) + .await?; + let generated_id = generate_exercise_id(&exercise_name, exercise_lot, user_id); + let exercise_id = match existing_exercise { + Some(db_ex) if db_ex.source == ExerciseSource::Github || db_ex.id == generated_id => { + db_ex.id + } + _ => match unique_exercises.get(exercise_name) { + Some(mem_ex) => mem_ex.id.clone(), + None => { + unique_exercises.insert( + exercise_name.clone(), + exercise::Model { + lot: exercise_lot, + id: generated_id.clone(), + name: exercise_name.to_owned(), + ..Default::default() + }, + ); + generated_id + } + }, + }; + Ok(exercise_id) + } } diff --git a/crates/services/importer/src/strong_app.rs b/crates/services/importer/src/strong_app.rs index ccd53a32bb..c104db1662 100644 --- a/crates/services/importer/src/strong_app.rs +++ b/crates/services/importer/src/strong_app.rs @@ -4,10 +4,9 @@ use async_graphql::Result; use chrono::{Duration, NaiveDateTime}; use common_utils::ryot_log; use csv::ReaderBuilder; -use database_models::{exercise, prelude::Exercise}; +use database_models::exercise; use dependent_models::{ImportCompletedItem, ImportResult}; -use dependent_utils::generate_exercise_id; -use enums::{ExerciseLot, ExerciseSource}; +use enums::ExerciseLot; use fitness_models::{ SetLot, UserExerciseInput, UserWorkoutInput, UserWorkoutSetRecord, WorkoutSetStatistic, }; @@ -17,7 +16,6 @@ use itertools::Itertools; use media_models::DeployStrongAppImportInput; use rust_decimal::Decimal; use rust_decimal_macros::dec; -use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; use serde::{Deserialize, Serialize}; use supporting_service::SupportingService; @@ -144,34 +142,14 @@ async fn import_exercises( }); continue; }; - let existing_exercise = Exercise::find() - .filter(exercise::Column::Lot.eq(exercise_lot)) - .filter(exercise::Column::Name.eq(&exercise_name)) - .one(&ss.db) - .await?; - let generated_id = generate_exercise_id(&exercise_name, exercise_lot, user_id); - let exercise_id = match existing_exercise { - Some(db_ex) - if db_ex.source == ExerciseSource::Github || db_ex.id == generated_id => - { - db_ex.id - } - _ => match unique_exercises.get(&exercise_name) { - Some(mem_ex) => mem_ex.id.clone(), - None => { - unique_exercises.insert( - exercise_name.clone(), - exercise::Model { - lot: exercise_lot, - name: exercise_name, - id: generated_id.clone(), - ..Default::default() - }, - ); - generated_id - } - }, - }; + let exercise_id = utils::associate_with_existing_or_new_exercise( + user_id, + &exercise_name, + exercise_lot, + ss, + &mut unique_exercises, + ) + .await?; ryot_log!(debug, "Importing exercise with id = {}", exercise_id); for set in exercises { if let Some(note) = set.notes { From 260b264bb7c9e471d5ba5a99ded43547dadc83c4 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Thu, 12 Dec 2024 00:12:40 +0530 Subject: [PATCH 08/17] chore(utils/dependent): schedule user for workout revision if workouts were imported --- crates/utils/dependent/src/lib.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/utils/dependent/src/lib.rs b/crates/utils/dependent/src/lib.rs index 3e693d8d99..11f7d641c8 100644 --- a/crates/utils/dependent/src/lib.rs +++ b/crates/utils/dependent/src/lib.rs @@ -2116,6 +2116,8 @@ where let source_result = import.clone(); let total = import.completed.len(); + let mut need_to_schedule_user_for_workout_revision = false; + for (idx, item) in import.completed.into_iter().enumerate() { ryot_log!( debug, @@ -2315,6 +2317,7 @@ where } } ImportCompletedItem::Workout(workout) => { + need_to_schedule_user_for_workout_revision = true; if let Err(err) = create_or_update_workout(workout, user_id, ss).await { import.failed.push(ImportFailedItem { lot: None, @@ -2366,6 +2369,10 @@ where .await?; } + if need_to_schedule_user_for_workout_revision { + schedule_user_for_workout_revision(user_id, ss).await?; + } + let details = ImportResultResponse { failed_items: import.failed, import: ImportDetails { total }, From 9f6eb19d350e8be3ac87cab2c43577a5044d7bc8 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Thu, 12 Dec 2024 00:20:10 +0530 Subject: [PATCH 09/17] refactor(services/importer): change order of attributes specified --- crates/services/importer/src/hevy.rs | 10 +++++----- crates/services/importer/src/strong_app.rs | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/services/importer/src/hevy.rs b/crates/services/importer/src/hevy.rs index 477a6c8797..70905dfaa3 100644 --- a/crates/services/importer/src/hevy.rs +++ b/crates/services/importer/src/hevy.rs @@ -122,6 +122,11 @@ pub async fn import( _ => SetLot::Normal, }; collected_sets.push(UserWorkoutSetRecord { + rpe: None, + note: None, + lot: set_lot, + rest_time: None, + confirmed_at: None, statistic: WorkoutSetStatistic { weight, reps: set.reps, @@ -129,11 +134,6 @@ pub async fn import( distance: set.distance.and_then(|d| d.checked_div(dec!(1000))), ..Default::default() }, - rpe: None, - note: None, - lot: set_lot, - rest_time: None, - confirmed_at: None, }); } collected_exercises.push(UserExerciseInput { diff --git a/crates/services/importer/src/strong_app.rs b/crates/services/importer/src/strong_app.rs index c104db1662..58e236c8a8 100644 --- a/crates/services/importer/src/strong_app.rs +++ b/crates/services/importer/src/strong_app.rs @@ -163,6 +163,11 @@ async fn import_exercises( _ => SetLot::Normal, }; collected_sets.push(UserWorkoutSetRecord { + rpe: None, + note: None, + lot: set_lot, + rest_time: None, + confirmed_at: None, statistic: WorkoutSetStatistic { weight, reps: set.reps, @@ -170,11 +175,6 @@ async fn import_exercises( distance: set.distance.and_then(|d| d.checked_div(dec!(1000))), ..Default::default() }, - rpe: None, - note: None, - lot: set_lot, - rest_time: None, - confirmed_at: None, }); } collected_exercises.push(UserExerciseInput { From b4efc46fdd782b99fdedd0430113ef5d776dbb5e Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Thu, 12 Dec 2024 00:20:39 +0530 Subject: [PATCH 10/17] feat(services/importer): also import the set rpe --- crates/services/importer/src/hevy.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/services/importer/src/hevy.rs b/crates/services/importer/src/hevy.rs index 70905dfaa3..83a9a97a3d 100644 --- a/crates/services/importer/src/hevy.rs +++ b/crates/services/importer/src/hevy.rs @@ -122,9 +122,9 @@ pub async fn import( _ => SetLot::Normal, }; collected_sets.push(UserWorkoutSetRecord { - rpe: None, note: None, lot: set_lot, + rpe: set.rpe, rest_time: None, confirmed_at: None, statistic: WorkoutSetStatistic { From ee28bcaecc4ce0aba5019a8f530491b9657dcca2 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Thu, 12 Dec 2024 00:36:25 +0530 Subject: [PATCH 11/17] feat(frontend): do not show graph if there are no values --- .../routes/_dashboard.fitness.exercises.item.$id._index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx b/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx index 10688affbc..ac961d86bc 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx @@ -539,7 +539,7 @@ export default function Page() { }; }); invariant(data); - return ( + return data.filter((d) => d.value).length > 0 ? ( @@ -557,7 +557,7 @@ export default function Page() { /> </Stack> </Paper> - ); + ) : null; })} </Stack> </Tabs.Panel> From eaa948a00931d4e7d6c89562cc5af00d8aa2dfc2 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri <ignisda2001@gmail.com> Date: Thu, 12 Dec 2024 00:36:48 +0530 Subject: [PATCH 12/17] refactor(frontend): change order of props --- .../routes/_dashboard.fitness.exercises.item.$id._index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx b/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx index ac961d86bc..5865853ed1 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx @@ -546,14 +546,14 @@ export default function Page() { {changeCase(best)} From e2f90452bdce09760c5f5b92ed3431a48ca10b3c Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Thu, 12 Dec 2024 00:36:59 +0530 Subject: [PATCH 13/17] ci: Run CI From 972cd0128d970b9ed40d1cd421e9bbd8730d40de Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Thu, 12 Dec 2024 18:17:57 +0530 Subject: [PATCH 14/17] feat(backend): export id as well for exercises --- Cargo.lock | 1 + crates/models/media/src/lib.rs | 6 ++++-- crates/services/exporter/Cargo.toml | 1 + crates/services/exporter/src/lib.rs | 29 ++++++++++++++++++++--------- docs/includes/export-schema.ts | 2 ++ 5 files changed, 28 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index beb9fee3a2..1b2fd34751 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2225,6 +2225,7 @@ dependencies = [ "dependent-models", "enums", "fitness-models", + "itertools 0.13.0", "media-models", "mime_guess", "nanoid", diff --git a/crates/models/media/src/lib.rs b/crates/models/media/src/lib.rs index 1455cd1e86..0a30fa432f 100644 --- a/crates/models/media/src/lib.rs +++ b/crates/models/media/src/lib.rs @@ -611,12 +611,14 @@ pub struct ImportOrExportPersonItem { #[derive(Debug, Serialize, Deserialize, Clone, Schematic)] #[serde(rename_all = "snake_case")] pub struct ImportOrExportExerciseItem { + /// The unique identifier of the exercise. + pub id: String, /// The name of the exercise. pub name: String, - /// The review history for the user. - pub reviews: Vec, /// The collections this entity was added to. pub collections: Vec, + /// The review history for the user. + pub reviews: Vec, } #[derive( diff --git a/crates/services/exporter/Cargo.toml b/crates/services/exporter/Cargo.toml index 963e05af9f..1ab50c39f4 100644 --- a/crates/services/exporter/Cargo.toml +++ b/crates/services/exporter/Cargo.toml @@ -13,6 +13,7 @@ database-models = { path = "../../models/database" } database-utils = { path = "../../utils/database" } dependent-models = { path = "../../models/dependent" } enums = { path = "../../enums" } +itertools = { workspace = true } fitness-models = { path = "../../models/fitness" } media-models = { path = "../../models/media" } mime_guess = { workspace = true } diff --git a/crates/services/exporter/src/lib.rs b/crates/services/exporter/src/lib.rs index 7414bf22b3..142c9ad834 100644 --- a/crates/services/exporter/src/lib.rs +++ b/crates/services/exporter/src/lib.rs @@ -19,6 +19,7 @@ use database_utils::{ use dependent_models::{ImportOrExportWorkoutItem, ImportOrExportWorkoutTemplateItem}; use enums::EntityLot; use fitness_models::UserMeasurementsListInput; +use itertools::Itertools; use media_models::{ ImportOrExportExerciseItem, ImportOrExportItemRating, ImportOrExportItemReview, ImportOrExportMetadataGroupItem, ImportOrExportMetadataItem, ImportOrExportMetadataItemSeen, @@ -373,27 +374,37 @@ impl ExporterService { user_id: &String, writer: &mut JsonStreamWriter, ) -> Result<()> { - let exercises = Exercise::find() - .filter(exercise::Column::CreatedByUserId.eq(user_id)) + let exercises = UserToEntity::find() + .select_only() + .column(exercise::Column::Id) + .column(exercise::Column::Name) + .filter(user_to_entity::Column::UserId.eq(user_id)) + .filter(user_to_entity::Column::ExerciseId.is_not_null()) + .left_join(Exercise) + .into_tuple::<(String, String)>() .all(&self.0.db) .await .unwrap(); - for e in exercises { - let reviews = item_reviews(user_id, &e.id, EntityLot::Exercise, false, &self.0) + for (exercise_id, exercise_name) in exercises { + let reviews = item_reviews(user_id, &exercise_id, EntityLot::Exercise, false, &self.0) .await? .into_iter() .map(|r| self.get_review_export_item(r)) - .collect(); + .collect_vec(); let collections = - entity_in_collections(&self.0.db, user_id, &e.id, EntityLot::Exercise) + entity_in_collections(&self.0.db, user_id, &exercise_id, EntityLot::Exercise) .await? .into_iter() .map(|c| c.name) - .collect(); + .collect_vec(); + if reviews.is_empty() && collections.is_empty() { + continue; + } let exp = ImportOrExportExerciseItem { - name: e.name, - collections, reviews, + collections, + id: exercise_id, + name: exercise_name, }; writer.serialize_value(&exp).unwrap(); } diff --git a/docs/includes/export-schema.ts b/docs/includes/export-schema.ts index a086bf6d67..7377e3f1f7 100644 --- a/docs/includes/export-schema.ts +++ b/docs/includes/export-schema.ts @@ -59,6 +59,8 @@ export interface ImportOrExportItemRating { export interface ImportOrExportExerciseItem { /** The collections this entity was added to. */ collections: string[]; + /** The unique identifier of the exercise. */ + id: string; /** The name of the exercise. */ name: string; /** The review history for the user. */ From 479b994e3a586e93886c0b8bd4fa71cdb8358f71 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Thu, 12 Dec 2024 18:31:12 +0530 Subject: [PATCH 15/17] refactor(backend): derive expiry from key names --- Cargo.lock | 1 + apps/backend/src/common.rs | 2 +- crates/providers/src/igdb.rs | 1 - crates/providers/src/listennotes.rs | 1 - crates/providers/src/tmdb.rs | 1 - crates/services/cache/Cargo.toml | 1 + crates/services/cache/src/lib.rs | 27 +++++++++++++++++++++--- crates/services/miscellaneous/src/lib.rs | 1 - crates/services/statistics/src/lib.rs | 2 -- crates/utils/dependent/src/lib.rs | 7 +----- 10 files changed, 28 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1b2fd34751..db0b0e1fda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1131,6 +1131,7 @@ dependencies = [ "chrono", "common-models", "common-utils", + "config", "database-models", "sea-orm", "sea-query", diff --git a/apps/backend/src/common.rs b/apps/backend/src/common.rs index 824b9fb401..03fd996ebd 100644 --- a/apps/backend/src/common.rs +++ b/apps/backend/src/common.rs @@ -69,7 +69,7 @@ pub async fn create_app_services( s3_client, config.file_storage.s3_bucket_name.clone(), )); - let cache_service = CacheService::new(&db); + let cache_service = CacheService::new(&db, config.clone()); let supporting_service = Arc::new( SupportingService::new( &db, diff --git a/crates/providers/src/igdb.rs b/crates/providers/src/igdb.rs index 50560ec15b..6e909712c4 100644 --- a/crates/providers/src/igdb.rs +++ b/crates/providers/src/igdb.rs @@ -547,7 +547,6 @@ impl IgdbService { let access_token = self.get_access_token().await; cc.set_with_expiry( ApplicationCacheKey::IgdbSettings, - None, ApplicationCacheValue::IgdbSettings { access_token: access_token.clone(), }, diff --git a/crates/providers/src/listennotes.rs b/crates/providers/src/listennotes.rs index 80aca4e33d..0b1ff82f0e 100644 --- a/crates/providers/src/listennotes.rs +++ b/crates/providers/src/listennotes.rs @@ -218,7 +218,6 @@ impl ListennotesService { } cc.set_with_expiry( ApplicationCacheKey::ListennotesSettings, - None, ApplicationCacheValue::ListennotesSettings { genres: genres.clone(), }, diff --git a/crates/providers/src/tmdb.rs b/crates/providers/src/tmdb.rs index 02f00a819c..eda378604a 100644 --- a/crates/providers/src/tmdb.rs +++ b/crates/providers/src/tmdb.rs @@ -1345,7 +1345,6 @@ async fn get_settings( }; cc.set_with_expiry( ApplicationCacheKey::TmdbSettings, - None, ApplicationCacheValue::TmdbSettings(settings.clone()), ) .await diff --git a/crates/services/cache/Cargo.toml b/crates/services/cache/Cargo.toml index db078fa18b..94dbc0c0a7 100644 --- a/crates/services/cache/Cargo.toml +++ b/crates/services/cache/Cargo.toml @@ -8,6 +8,7 @@ async-graphql = { workspace = true } chrono = { workspace = true } common-models = { path = "../../models/common" } common-utils = { path = "../../utils/common" } +config = { path = "../../config" } database-models = { path = "../../models/database" } sea-orm = { workspace = true } sea-query = { workspace = true } diff --git a/crates/services/cache/src/lib.rs b/crates/services/cache/src/lib.rs index 727c2784e4..478896301e 100644 --- a/crates/services/cache/src/lib.rs +++ b/crates/services/cache/src/lib.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use async_graphql::Result; use chrono::{Duration, Utc}; use common_models::{ApplicationCacheKey, ApplicationCacheValue}; @@ -9,22 +11,41 @@ use uuid::Uuid; pub struct CacheService { db: DatabaseConnection, + config: Arc, } impl CacheService { - pub fn new(db: &DatabaseConnection) -> Self { - Self { db: db.clone() } + pub fn new(db: &DatabaseConnection, config: Arc) -> Self { + Self { + config, + db: db.clone(), + } } } impl CacheService { + fn get_expiry_for_key(&self, key: &ApplicationCacheKey) -> Option { + match key { + ApplicationCacheKey::UserAnalyticsParameters { .. } => Some(8), + ApplicationCacheKey::UserAnalytics { .. } => Some(2), + ApplicationCacheKey::IgdbSettings + | ApplicationCacheKey::ListennotesSettings + | ApplicationCacheKey::ServerKeyValidated + | ApplicationCacheKey::TmdbSettings => None, + ApplicationCacheKey::MetadataRecentlyConsumed { .. } => Some(1), + ApplicationCacheKey::ProgressUpdateCache { .. } => { + Some(self.config.server.progress_update_threshold) + } + } + } + pub async fn set_with_expiry( &self, key: ApplicationCacheKey, - expiry_hours: Option, value: ApplicationCacheValue, ) -> Result { let now = Utc::now(); + let expiry_hours = self.get_expiry_for_key(&key); let to_insert = application_cache::ActiveModel { key: ActiveValue::Set(key), value: ActiveValue::Set(value), diff --git a/crates/services/miscellaneous/src/lib.rs b/crates/services/miscellaneous/src/lib.rs index 772070bbce..eb69c734fb 100644 --- a/crates/services/miscellaneous/src/lib.rs +++ b/crates/services/miscellaneous/src/lib.rs @@ -3140,7 +3140,6 @@ ORDER BY RANDOM() LIMIT 10; if is_server_key_validated { cs.set_with_expiry( ApplicationCacheKey::ServerKeyValidated, - None, ApplicationCacheValue::Empty, ) .await?; diff --git a/crates/services/statistics/src/lib.rs b/crates/services/statistics/src/lib.rs index e32c23f900..845f66a9f5 100644 --- a/crates/services/statistics/src/lib.rs +++ b/crates/services/statistics/src/lib.rs @@ -66,7 +66,6 @@ impl StatisticsService { .cache_service .set_with_expiry( cache_key, - Some(8), ApplicationCacheValue::UserAnalyticsParameters(response.clone()), ) .await?; @@ -379,7 +378,6 @@ impl StatisticsService { .cache_service .set_with_expiry( cache_key, - Some(2), ApplicationCacheValue::UserAnalytics(response.clone()), ) .await?; diff --git a/crates/utils/dependent/src/lib.rs b/crates/utils/dependent/src/lib.rs index 11f7d641c8..575bab68b9 100644 --- a/crates/utils/dependent/src/lib.rs +++ b/crates/utils/dependent/src/lib.rs @@ -1264,7 +1264,6 @@ pub async fn mark_entity_as_recently_consumed( user_id: user_id.to_owned(), entity_id: entity_id.to_owned(), }, - Some(1), ApplicationCacheValue::Empty, ) .await?; @@ -1527,11 +1526,7 @@ pub async fn progress_update( let id = seen.id.clone(); if seen.state == SeenState::Completed && respect_cache { ss.cache_service - .set_with_expiry( - cache, - Some(ss.config.server.progress_update_threshold), - ApplicationCacheValue::Empty, - ) + .set_with_expiry(cache, ApplicationCacheValue::Empty) .await?; } if seen.state == SeenState::Completed { From 2a1f439f9e6c2b35e98a9a3d2fda99326ff39c5b Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Thu, 12 Dec 2024 18:36:05 +0530 Subject: [PATCH 16/17] refactor(services/importer): fn to parse a date string --- crates/services/importer/src/hevy.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/services/importer/src/hevy.rs b/crates/services/importer/src/hevy.rs index 83a9a97a3d..074ef0355e 100644 --- a/crates/services/importer/src/hevy.rs +++ b/crates/services/importer/src/hevy.rs @@ -147,10 +147,8 @@ pub async fn import( .unwrap_or_default(), }); } - let start_time = - NaiveDateTime::parse_from_str(&first_exercise.start_time, "%d %b %Y, %H:%M").unwrap(); - let end_time = - NaiveDateTime::parse_from_str(&first_exercise.end_time, "%d %b %Y, %H:%M").unwrap(); + let start_time = parse_date_string(&first_exercise.start_time); + let end_time = parse_date_string(&first_exercise.end_time); completed.push(ImportCompletedItem::Workout(UserWorkoutInput { assets: None, supersets: vec![], @@ -174,3 +172,7 @@ pub async fn import( ); Ok(ImportResult { failed, completed }) } + +fn parse_date_string(input: &str) -> NaiveDateTime { + NaiveDateTime::parse_from_str(&input, "%d %b %Y, %H:%M").unwrap() +} From 52cb1584f877c4754469170a014f7f37d876d295 Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Thu, 12 Dec 2024 18:45:06 +0530 Subject: [PATCH 17/17] chore(backend): remove as many usages of `crate::` as possible --- .../m20240903_add_changes_for_user_to_collection_removal.rs | 2 +- .../m20240926_add_columns_for_open_sourcing_pro_version.rs | 2 +- crates/migrations/src/m20241019_changes_for_issue_929.rs | 2 +- crates/migrations/src/m20241126_changes_for_issue_1113.rs | 2 +- crates/models/database/src/functions.rs | 2 +- crates/models/database/src/review.rs | 2 +- crates/models/database/src/seen.rs | 2 +- crates/services/importer/src/audiobookshelf.rs | 4 +--- crates/services/importer/src/goodreads.rs | 4 +--- crates/services/importer/src/hevy.rs | 2 +- crates/services/importer/src/storygraph.rs | 4 +--- 11 files changed, 11 insertions(+), 17 deletions(-) diff --git a/crates/migrations/src/m20240903_add_changes_for_user_to_collection_removal.rs b/crates/migrations/src/m20240903_add_changes_for_user_to_collection_removal.rs index da95d0ed8f..8b539013f9 100644 --- a/crates/migrations/src/m20240903_add_changes_for_user_to_collection_removal.rs +++ b/crates/migrations/src/m20240903_add_changes_for_user_to_collection_removal.rs @@ -1,6 +1,6 @@ use sea_orm_migration::prelude::*; -use crate::m20231017_create_user_to_entity::CONSTRAINT_SQL; +use super::m20231017_create_user_to_entity::CONSTRAINT_SQL; #[derive(DeriveMigrationName)] pub struct Migration; diff --git a/crates/migrations/src/m20240926_add_columns_for_open_sourcing_pro_version.rs b/crates/migrations/src/m20240926_add_columns_for_open_sourcing_pro_version.rs index 523dc81ef2..12bb9f29db 100644 --- a/crates/migrations/src/m20240926_add_columns_for_open_sourcing_pro_version.rs +++ b/crates/migrations/src/m20240926_add_columns_for_open_sourcing_pro_version.rs @@ -1,6 +1,6 @@ use sea_orm_migration::prelude::*; -use crate::{ +use super::{ m20231016_create_collection_to_entity::{CONSTRAINT_SQL, ENTITY_ID_SQL, ENTITY_LOT_SQL}, m20240904_create_monitored_entity::MONITORED_ENTITY_VIEW_CREATION_SQL, }; diff --git a/crates/migrations/src/m20241019_changes_for_issue_929.rs b/crates/migrations/src/m20241019_changes_for_issue_929.rs index 0544bb07a1..e702e1c863 100644 --- a/crates/migrations/src/m20241019_changes_for_issue_929.rs +++ b/crates/migrations/src/m20241019_changes_for_issue_929.rs @@ -1,6 +1,6 @@ use sea_orm_migration::prelude::*; -use crate::m20240904_create_monitored_entity::MONITORED_ENTITY_VIEW_CREATION_SQL; +use super::m20240904_create_monitored_entity::MONITORED_ENTITY_VIEW_CREATION_SQL; #[derive(DeriveMigrationName)] pub struct Migration; diff --git a/crates/migrations/src/m20241126_changes_for_issue_1113.rs b/crates/migrations/src/m20241126_changes_for_issue_1113.rs index 3a18b387a8..e8da7f4460 100644 --- a/crates/migrations/src/m20241126_changes_for_issue_1113.rs +++ b/crates/migrations/src/m20241126_changes_for_issue_1113.rs @@ -1,6 +1,6 @@ use sea_orm_migration::prelude::*; -use crate::{ +use super::{ m20230822_create_exercise::EXERCISE_NAME_INDEX, m20240827_create_daily_user_activity::create_daily_user_activity_table, }; diff --git a/crates/models/database/src/functions.rs b/crates/models/database/src/functions.rs index 4f4035887a..752512ac44 100644 --- a/crates/models/database/src/functions.rs +++ b/crates/models/database/src/functions.rs @@ -5,7 +5,7 @@ use sea_orm::{ ActiveModelTrait, ActiveValue, ColumnTrait, ConnectionTrait, EntityTrait, QueryFilter, }; -use crate::{prelude::UserToEntity, user_to_entity}; +use super::{prelude::UserToEntity, user_to_entity}; pub async fn get_user_to_entity_association( db: &C, diff --git a/crates/models/database/src/review.rs b/crates/models/database/src/review.rs index 0964ba387e..4eb462039b 100644 --- a/crates/models/database/src/review.rs +++ b/crates/models/database/src/review.rs @@ -11,7 +11,7 @@ use rust_decimal::Decimal; use sea_orm::{entity::prelude::*, ActiveValue}; use serde::{Deserialize, Serialize}; -use crate::functions::associate_user_with_entity; +use super::functions::associate_user_with_entity; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[sea_orm(table_name = "review")] diff --git a/crates/models/database/src/seen.rs b/crates/models/database/src/seen.rs index bc3d893b70..088b61025d 100644 --- a/crates/models/database/src/seen.rs +++ b/crates/models/database/src/seen.rs @@ -15,7 +15,7 @@ use rust_decimal_macros::dec; use sea_orm::{entity::prelude::*, ActiveValue}; use serde::{Deserialize, Serialize}; -use crate::functions::associate_user_with_entity; +use super::functions::associate_user_with_entity; #[derive(Clone, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize, SimpleObject, Educe)] #[graphql(name = "Seen")] diff --git a/crates/services/importer/src/audiobookshelf.rs b/crates/services/importer/src/audiobookshelf.rs index e604c1411a..ce1b577638 100644 --- a/crates/services/importer/src/audiobookshelf.rs +++ b/crates/services/importer/src/audiobookshelf.rs @@ -20,9 +20,7 @@ use reqwest::{ use serde_json::json; use specific_models::audiobookshelf as audiobookshelf_models; -use crate::utils; - -use super::{ImportFailStep, ImportFailedItem}; +use super::{utils, ImportFailStep, ImportFailedItem}; pub async fn import( input: DeployUrlAndKeyImportInput, diff --git a/crates/services/importer/src/goodreads.rs b/crates/services/importer/src/goodreads.rs index f497399a35..d2304e4340 100644 --- a/crates/services/importer/src/goodreads.rs +++ b/crates/services/importer/src/goodreads.rs @@ -15,9 +15,7 @@ use rust_decimal::Decimal; use rust_decimal_macros::dec; use serde::Deserialize; -use crate::utils; - -use super::{ImportFailStep, ImportFailedItem}; +use super::{utils, ImportFailStep, ImportFailedItem}; #[derive(Debug, Deserialize)] struct Book { diff --git a/crates/services/importer/src/hevy.rs b/crates/services/importer/src/hevy.rs index 074ef0355e..91a2a7ac54 100644 --- a/crates/services/importer/src/hevy.rs +++ b/crates/services/importer/src/hevy.rs @@ -19,7 +19,7 @@ use rust_decimal_macros::dec; use serde::{Deserialize, Serialize}; use supporting_service::SupportingService; -use crate::utils; +use super::utils; #[derive(Debug, Serialize, Deserialize, Clone, Default)] struct Entry { diff --git a/crates/services/importer/src/storygraph.rs b/crates/services/importer/src/storygraph.rs index 0800b0dd21..457470937e 100644 --- a/crates/services/importer/src/storygraph.rs +++ b/crates/services/importer/src/storygraph.rs @@ -15,9 +15,7 @@ use rust_decimal::Decimal; use rust_decimal_macros::dec; use serde::{Deserialize, Serialize}; -use crate::utils; - -use super::{ImportFailStep, ImportFailedItem, ImportOrExportMetadataItem}; +use super::{utils, ImportFailStep, ImportFailedItem, ImportOrExportMetadataItem}; #[derive(Debug, Serialize, Deserialize)] #[serde(untagged)]