From d9214a82ca5d2f85d6af528cbdbd8d569b8a37ba Mon Sep 17 00:00:00 2001 From: Diptesh Choudhuri Date: Mon, 24 Jun 2024 10:06:08 +0530 Subject: [PATCH] feat(backend): allow committing collections from yank integrations progress And other changes. --- apps/backend/src/fitness/logic.rs | 9 +- apps/backend/src/fitness/resolver.rs | 3 +- apps/backend/src/importer/audiobookshelf.rs | 15 +- apps/backend/src/importer/strong_app.rs | 1 + apps/backend/src/integrations.rs | 28 +- apps/backend/src/miscellaneous/mod.rs | 10 + apps/backend/src/miscellaneous/resolver.rs | 35 +- apps/backend/src/models.rs | 4 +- apps/backend/src/utils.rs | 3 +- .../_dashboard.fitness.workouts.current.tsx | 433 +++++++++--------- docs/includes/export-schema.ts | 1 + libs/generated/src/graphql/backend/graphql.ts | 2 + 12 files changed, 295 insertions(+), 249 deletions(-) diff --git a/apps/backend/src/fitness/logic.rs b/apps/backend/src/fitness/logic.rs index 7dc844b7b4..86dc417cd2 100644 --- a/apps/backend/src/fitness/logic.rs +++ b/apps/backend/src/fitness/logic.rs @@ -203,12 +203,13 @@ impl UserWorkoutInput { totals.weight = Some(we * Decimal::from_usize(*re).unwrap()); } let mut value = WorkoutSetRecord { - statistic: set.statistic.clone(), - lot: set.lot, - confirmed_at: set.confirmed_at, totals, - personal_bests: vec![], + lot: set.lot, actual_rest_time, + note: set.note.clone(), + personal_bests: vec![], + confirmed_at: set.confirmed_at, + statistic: set.statistic.clone(), }; value.statistic.one_rm = value.calculate_one_rm(); value.statistic.pace = value.calculate_pace(); diff --git a/apps/backend/src/fitness/resolver.rs b/apps/backend/src/fitness/resolver.rs index a6e538af7c..60930af1ea 100644 --- a/apps/backend/src/fitness/resolver.rs +++ b/apps/backend/src/fitness/resolver.rs @@ -886,8 +886,9 @@ impl ExerciseService { .sets .into_iter() .map(|s| UserWorkoutSetRecord { - statistic: s.statistic, lot: s.lot, + note: s.note, + statistic: s.statistic, confirmed_at: s.confirmed_at, }) .collect(), diff --git a/apps/backend/src/importer/audiobookshelf.rs b/apps/backend/src/importer/audiobookshelf.rs index 1be5d48b35..05570e9bcf 100644 --- a/apps/backend/src/importer/audiobookshelf.rs +++ b/apps/backend/src/importer/audiobookshelf.rs @@ -8,7 +8,6 @@ use reqwest::{ header::{HeaderValue, AUTHORIZATION}, Client, }; -use serde::{Deserialize, Serialize}; use serde_json::json; use crate::{ @@ -25,16 +24,6 @@ use crate::{ use super::DeployUrlAndKeyImportInput; -#[derive(Debug, Serialize, Deserialize)] -pub struct LibrariesListResponse { - pub libraries: Vec, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ListResponse { - pub results: Vec, -} - pub async fn import( input: DeployUrlAndKeyImportInput, isbn_service: &GoogleBooksService, @@ -57,7 +46,7 @@ where .send() .await .map_err(|e| anyhow!(e))? - .json::() + .json::() .await .unwrap(); for library in libraries_resp.libraries { @@ -72,7 +61,7 @@ where .send() .await .map_err(|e| anyhow!(e))? - .json::() + .json::() .await .unwrap(); let len = finished_items.results.len(); diff --git a/apps/backend/src/importer/strong_app.rs b/apps/backend/src/importer/strong_app.rs index 46d4c8c0c4..180b3d813a 100644 --- a/apps/backend/src/importer/strong_app.rs +++ b/apps/backend/src/importer/strong_app.rs @@ -87,6 +87,7 @@ pub async fn import( weight, ..Default::default() }, + note: None, lot: SetLot::Normal, confirmed_at: None, }); diff --git a/apps/backend/src/integrations.rs b/apps/backend/src/integrations.rs index 021a9e7bc4..5c86147ced 100644 --- a/apps/backend/src/integrations.rs +++ b/apps/backend/src/integrations.rs @@ -20,7 +20,7 @@ use crate::{ }; #[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct IntegrationMedia { +pub struct IntegrationMediaSeen { pub identifier: String, pub lot: MediaLot, #[serde(default)] @@ -35,6 +35,14 @@ pub struct IntegrationMedia { pub provider_watched_on: Option, } +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct IntegrationMediaCollection { + pub identifier: String, + pub lot: MediaLot, + pub source: MediaSource, + pub collection: String, +} + #[derive(Debug)] pub struct IntegrationService { db: DatabaseConnection, @@ -45,7 +53,7 @@ impl IntegrationService { Self { db: db.clone() } } - pub async fn jellyfin_progress(&self, payload: &str) -> Result { + pub async fn jellyfin_progress(&self, payload: &str) -> Result { mod models { use super::*; @@ -112,7 +120,7 @@ impl IntegrationService { "Movie" => MediaLot::Movie, _ => bail!("Only movies and shows supported"), }; - Ok(IntegrationMedia { + Ok(IntegrationMediaSeen { identifier, lot, source: MediaSource::Tmdb, @@ -128,7 +136,7 @@ impl IntegrationService { &self, payload: &str, plex_user: Option, - ) -> Result { + ) -> Result { mod models { use super::*; @@ -238,7 +246,7 @@ impl IntegrationService { }, }; - Ok(IntegrationMedia { + Ok(IntegrationMediaSeen { identifier, lot, source: MediaSource::Tmdb, @@ -250,8 +258,8 @@ impl IntegrationService { }) } - pub async fn kodi_progress(&self, payload: &str) -> Result { - let mut payload = match serde_json::from_str::(payload) { + pub async fn kodi_progress(&self, payload: &str) -> Result { + let mut payload = match serde_json::from_str::(payload) { Result::Ok(val) => val, Result::Err(err) => bail!(err), }; @@ -266,7 +274,7 @@ impl IntegrationService { access_token: &str, isbn_service: &GoogleBooksService, commit_metadata: impl Fn(CommitMediaInput) -> F, - ) -> Result> + ) -> Result<(Vec, Vec)> where F: Future>, { @@ -372,7 +380,7 @@ impl IntegrationService { } else { resp.progress }; - media_items.push(IntegrationMedia { + media_items.push(IntegrationMediaSeen { lot, source, identifier, @@ -388,6 +396,6 @@ impl IntegrationService { } }; } - Ok(media_items) + Ok((media_items, vec![])) } } diff --git a/apps/backend/src/miscellaneous/mod.rs b/apps/backend/src/miscellaneous/mod.rs index 9cf27afc4f..546e8deb69 100644 --- a/apps/backend/src/miscellaneous/mod.rs +++ b/apps/backend/src/miscellaneous/mod.rs @@ -156,6 +156,16 @@ pub mod audiobookshelf_models { pub struct Response { pub library_items: Vec, } + + #[derive(Debug, Serialize, Deserialize)] + pub struct LibrariesListResponse { + pub libraries: Vec, + } + + #[derive(Debug, Serialize, Deserialize)] + pub struct ListResponse { + pub results: Vec, + } } pub fn itunes_podcast_episode_by_name(name: &str, podcast: metadata::Model) -> Option { diff --git a/apps/backend/src/miscellaneous/resolver.rs b/apps/backend/src/miscellaneous/resolver.rs index f17521ed44..5067bfe8ea 100644 --- a/apps/backend/src/miscellaneous/resolver.rs +++ b/apps/backend/src/miscellaneous/resolver.rs @@ -68,7 +68,7 @@ use crate::{ }, file_storage::FileStorageService, fitness::resolver::ExerciseService, - integrations::{IntegrationMedia, IntegrationService}, + integrations::{IntegrationMediaSeen, IntegrationService}, jwt, miscellaneous::{CustomService, DefaultCollection}, models::{ @@ -5572,6 +5572,7 @@ impl MiscellaneousService { .all(&self.db) .await?; let mut progress_updates = vec![]; + let mut collection_updates = vec![]; let mut to_update_integrations = vec![]; for integration in integrations.into_iter() { let response = match integration.source { @@ -5588,16 +5589,38 @@ impl MiscellaneousService { } _ => continue, }; - if let Ok(data) = response { - progress_updates.extend(data); + if let Ok((seen_progress, collection_progress)) = response { + progress_updates.extend(seen_progress); + collection_updates.extend(collection_progress); to_update_integrations.push(integration.id); } } - for pu in progress_updates.into_iter() { - self.integration_progress_update(pu, user_id) + for progress_update in progress_updates.into_iter() { + self.integration_progress_update(progress_update, user_id) .await .trace_ok(); } + for col_update in collection_updates.into_iter() { + let metadata::Model { id, .. } = self + .commit_metadata(CommitMediaInput { + lot: col_update.lot, + source: col_update.source, + identifier: col_update.identifier.clone(), + force_update: None, + }) + .await?; + self.add_entity_to_collection( + user_id, + ChangeCollectionToEntityInput { + creator_user_id: user_id.to_owned(), + collection_name: col_update.collection, + metadata_id: Some(id.clone()), + ..Default::default() + }, + ) + .await + .trace_ok(); + } Integration::update_many() .filter(integration::Column::Id.is_in(to_update_integrations)) .col_expr( @@ -5702,7 +5725,7 @@ impl MiscellaneousService { #[tracing::instrument(skip(self))] async fn integration_progress_update( &self, - pu: IntegrationMedia, + pu: IntegrationMediaSeen, user_id: &String, ) -> Result<()> { let maximum_limit = diff --git a/apps/backend/src/models.rs b/apps/backend/src/models.rs index 2f800b755a..7bcfb59957 100644 --- a/apps/backend/src/models.rs +++ b/apps/backend/src/models.rs @@ -1652,6 +1652,7 @@ pub mod fitness { #[serde(default)] pub totals: WorkoutSetTotals, pub actual_rest_time: Option, + pub note: Option, } impl WorkoutSetRecord { @@ -1878,8 +1879,9 @@ pub mod fitness { #[derive(Clone, Debug, Deserialize, Serialize, InputObject)] pub struct UserWorkoutSetRecord { - pub statistic: WorkoutSetStatistic, pub lot: SetLot, + pub note: Option, + pub statistic: WorkoutSetStatistic, pub confirmed_at: Option, } diff --git a/apps/backend/src/utils.rs b/apps/backend/src/utils.rs index a89060ec10..8ab1ea3a5f 100644 --- a/apps/backend/src/utils.rs +++ b/apps/backend/src/utils.rs @@ -352,7 +352,8 @@ pub async fn add_entity_to_collection( information: ActiveValue::Set(information), ..Default::default() }; - if created_collection.insert(db).await.is_ok() { + if let Ok(created) = created_collection.insert(db).await { + tracing::debug!("Created collection to entity: {:?}", created); associate_user_with_entity( user_id, input.metadata_id, diff --git a/apps/frontend/app/routes/_dashboard.fitness.workouts.current.tsx b/apps/frontend/app/routes/_dashboard.fitness.workouts.current.tsx index 65605d4f58..3edf4733e8 100644 --- a/apps/frontend/app/routes/_dashboard.fitness.workouts.current.tsx +++ b/apps/frontend/app/routes/_dashboard.fitness.workouts.current.tsx @@ -84,7 +84,7 @@ import { produce } from "immer"; import { useAtom } from "jotai"; import { RESET } from "jotai/utils"; import Cookies from "js-cookie"; -import { useEffect, useRef, useState } from "react"; +import { Fragment, useEffect, useRef, useState } from "react"; import Webcam from "react-webcam"; import { ClientOnly } from "remix-utils/client-only"; import { namedAction } from "remix-utils/named-action"; @@ -1071,235 +1071,242 @@ const ExerciseDisplay = (props: { {props.exercise.sets.map((s, idx) => ( - - - - - - {match(s.lot) - .with(SetLot.Normal, () => idx + 1) - .otherwise(() => s.lot.at(0))} - - - - - Set type - {Object.values(SetLot).map((lot) => ( + + + + + + + {match(s.lot) + .with(SetLot.Normal, () => idx + 1) + .otherwise(() => s.lot.at(0))} + + + + + Set type + {Object.values(SetLot).map((lot) => ( + + {lot.at(0)} + + } + onClick={() => { + setCurrentWorkout( + produce(currentWorkout, (draft) => { + draft.exercises[props.exerciseIdx].sets[ + idx + ].lot = lot; + }), + ); + }} + > + {startCase(snakeCase(lot))} + + ))} + + Actions - {lot.at(0)} - - } + leftSection={} onClick={() => { - setCurrentWorkout( - produce(currentWorkout, (draft) => { - draft.exercises[props.exerciseIdx].sets[idx].lot = - lot; - }), - ); + const yes = match(s.confirmed) + .with(true, () => { + return confirm( + "Are you sure you want to delete this set?", + ); + }) + .with(false, () => true) + .exhaustive(); + if (yes) + setCurrentWorkout( + produce(currentWorkout, (draft) => { + draft.exercises[props.exerciseIdx].sets.splice( + idx, + 1, + ); + }), + ); }} > - {startCase(snakeCase(lot))} + Delete - ))} - - Actions - } - onClick={() => { - const yes = match(s.confirmed) - .with(true, () => { - return confirm( - "Are you sure you want to delete this set?", - ); - }) - .with(false, () => true) - .exhaustive(); - if (yes) + + + + {props.exercise.alreadyDoneSets[idx] ? ( + { + if (props.exercise.sets[idx].confirmed) return; + const convertStringValuesToNumbers = ( + obj: Record, + ) => { + const newObject = { ...obj }; + for (const key in newObject) + if ( + typeof newObject[key] === "string" && + !Number.isNaN(newObject[key]) + ) + newObject[key] = Number.parseFloat( + newObject[key] as string, + ); + return newObject; + }; setCurrentWorkout( produce(currentWorkout, (draft) => { - draft.exercises[props.exerciseIdx].sets.splice( - idx, - 1, - ); + if (draft) { + draft.exercises[props.exerciseIdx].sets[ + idx + ].statistic = convertStringValuesToNumbers( + props.exercise.alreadyDoneSets[idx].statistic, + ); + } }), ); + }} + style={ + !props.exercise.sets[idx].confirmed + ? { cursor: "pointer" } + : undefined + } + > + + + ) : ( + "—" + )} + + {durationCol ? ( + + ) : null} + {distanceCol ? ( + + ) : null} + {weightCol ? ( + + ) : null} + {repsCol ? ( + + ) : null} + + - Delete - - - - - {props.exercise.alreadyDoneSets[idx] ? ( - { - if (props.exercise.sets[idx].confirmed) return; - const convertStringValuesToNumbers = ( - obj: Record, - ) => { - const newObject = { ...obj }; - for (const key in newObject) + {(style) => ( + + typeof s.statistic.distance === "number" && + typeof s.statistic.duration === "number", + ) + .with( + ExerciseLot.Duration, + () => typeof s.statistic.duration === "number", + ) + .with( + ExerciseLot.Reps, + () => typeof s.statistic.reps === "number", + ) + .with( + ExerciseLot.RepsAndWeight, + () => + typeof s.statistic.reps === "number" && + typeof s.statistic.weight === "number", + ) + .exhaustive() + } + color="green" + onClick={() => { + playCheckSound(); + const newConfirmed = !s.confirmed; if ( - typeof newObject[key] === "string" && - !Number.isNaN(newObject[key]) + !newConfirmed && + currentTimer?.triggeredBy?.exerciseIdentifier === + props.exercise.identifier && + currentTimer?.triggeredBy?.setIdx === idx ) - newObject[key] = Number.parseFloat( - newObject[key] as string, - ); - return newObject; - }; - setCurrentWorkout( - produce(currentWorkout, (draft) => { - if (draft) { - draft.exercises[props.exerciseIdx].sets[ - idx - ].statistic = convertStringValuesToNumbers( - props.exercise.alreadyDoneSets[idx].statistic, + props.stopTimer(); + if ( + props.exercise.restTimer?.enabled && + newConfirmed && + s.lot !== SetLot.WarmUp + ) { + props.startTimer( + props.exercise.restTimer.duration, + { + exerciseIdentifier: props.exercise.identifier, + setIdx: idx, + }, ); } - }), - ); - }} - style={ - !props.exercise.sets[idx].confirmed - ? { cursor: "pointer" } - : undefined - } - > - - - ) : ( - "—" - )} - - {durationCol ? ( - - ) : null} - {distanceCol ? ( - - ) : null} - {weightCol ? ( - - ) : null} - {repsCol ? ( - - ) : null} - - - {(style) => ( - - typeof s.statistic.distance === "number" && - typeof s.statistic.duration === "number", - ) - .with( - ExerciseLot.Duration, - () => typeof s.statistic.duration === "number", - ) - .with( - ExerciseLot.Reps, - () => typeof s.statistic.reps === "number", - ) - .with( - ExerciseLot.RepsAndWeight, - () => - typeof s.statistic.reps === "number" && - typeof s.statistic.weight === "number", - ) - .exhaustive() - } - color="green" - onClick={() => { - playCheckSound(); - const newConfirmed = !s.confirmed; - if ( - !newConfirmed && - currentTimer?.triggeredBy?.exerciseIdentifier === - props.exercise.identifier && - currentTimer?.triggeredBy?.setIdx === idx - ) - props.stopTimer(); - if ( - props.exercise.restTimer?.enabled && - newConfirmed && - s.lot !== SetLot.WarmUp - ) { - props.startTimer( - props.exercise.restTimer.duration, - { - exerciseIdentifier: props.exercise.identifier, - setIdx: idx, - }, + setCurrentWorkout( + produce(currentWorkout, (draft) => { + const currentExercise = + draft.exercises[props.exerciseIdx]; + currentExercise.sets[idx].confirmed = + newConfirmed; + currentExercise.sets[idx].confirmedAt = + dayjsLib().toISOString(); + }), ); - } - setCurrentWorkout( - produce(currentWorkout, (draft) => { - const currentExercise = - draft.exercises[props.exerciseIdx]; - currentExercise.sets[idx].confirmed = - newConfirmed; - currentExercise.sets[idx].confirmedAt = - dayjsLib().toISOString(); - }), - ); - }} - data-statistics={JSON.stringify(s.statistic)} - > - - - )} - - - + }} + data-statistics={JSON.stringify(s.statistic)} + > + + + )} + + + + ))}