Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RequestParam

@FeignClient(
value = "TrainerRoadApiClient",
Expand All @@ -18,11 +20,23 @@ import org.springframework.web.bind.annotation.RequestBody
configuration = [TrainerRoadApiClientConfig::class]
)
interface TrainerRoadApiClient {
@GetMapping("/app/api/calendar/activities/{username}?startDate={startDate}&endDate={endDate}")
@GetMapping(
value = ["/app/api/react-calendar/{memberId}/timeline"],
headers = ["trainerroad-jsonformat=camel-case", "tr-cache-control=use-cache"]
)
fun getTimeline(
@PathVariable("memberId") memberId: Long,
@RequestParam("start") startDate: String,
@RequestParam("end") endDate: String,
): TrainerRoadTimelineDTO

@GetMapping(
value = ["/app/api/react-calendar/{memberId}/activities"],
headers = ["trainerroad-jsonformat=camel-case", "tr-cache-control=use-cache"]
)
fun getActivities(
@PathVariable("username") username: String,
@PathVariable("startDate") startDate: String,
@PathVariable("endDate") endDate: String,
@PathVariable("memberId") memberId: Long,
@RequestHeader("ids") ids: String,
): List<TrainerRoadActivityDTO>

@GetMapping("/app/api/workouts")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package org.freekode.tp2intervals.infrastructure.platform.trainerroad
import org.freekode.tp2intervals.domain.activity.Activity
import org.freekode.tp2intervals.domain.workout.Workout
import org.freekode.tp2intervals.domain.workout.WorkoutDetails
import org.freekode.tp2intervals.domain.workout.structure.SingleStep
import org.freekode.tp2intervals.infrastructure.platform.trainerroad.activity.TrainerRoadActivityDTO
import org.freekode.tp2intervals.infrastructure.platform.trainerroad.activity.TrainerRoadActivityMapper
import org.freekode.tp2intervals.infrastructure.platform.trainerroad.configuration.TrainerRoadConfigurationRepository
import org.freekode.tp2intervals.infrastructure.platform.trainerroad.workout.TrainerRoadWorkoutMapper
import org.slf4j.LoggerFactory
import org.springframework.cache.annotation.CacheConfig
import org.springframework.cache.annotation.Cacheable
import org.springframework.stereotype.Repository
Expand All @@ -18,18 +20,22 @@ class TrainerRoadApiClientService(
private val trainerRoadApiClient: TrainerRoadApiClient,
private val trainerRoadConfigurationRepository: TrainerRoadConfigurationRepository,
) {
private val log = LoggerFactory.getLogger(this.javaClass)

fun findWorkoutsFromLibraryByName(name: String): List<WorkoutDetails> {
val removeHtmlTags = trainerRoadConfigurationRepository.getConfiguration().removeHtmlTags
return trainerRoadApiClient.findWorkouts(TRFindWorkoutsRequestDTO(name, 0, 500)).workouts
.map { TrainerRoadWorkoutMapper().toWorkoutDetails(it, removeHtmlTags) }
}

fun getWorkoutsFromCalendar(startDate: LocalDate, endDate: LocalDate, username: String): List<Workout> {
return trainerRoadApiClient.getActivities(username, startDate.toString(), endDate.toString())
.filter { it.activity != null }
.map { activity ->
getWorkout(activity.activity!!.id)
.withDate(activity.date.toLocalDate())
fun getWorkoutsFromCalendar(startDate: LocalDate, endDate: LocalDate, memberId: Long): List<Workout> {
return trainerRoadApiClient.getTimeline(memberId, startDate.toString(), endDate.toString())
.plannedActivities
.filter { it.date.toLocalDate() in startDate..endDate }
.filter { it.workoutId != null }
.map { plannedActivity ->
getWorkout(plannedActivity.workoutId!!.toString())
.withDate(plannedActivity.date.toLocalDate())
}
}

Expand All @@ -39,16 +45,45 @@ class TrainerRoadApiClientService(
val trainerRoadWorkoutMapper = TrainerRoadWorkoutMapper()
return trainerRoadApiClient.getWorkout(trWorkoutId)
.let { trainerRoadWorkoutMapper.toWorkout(it, removeHtmlTags) }
.also { workout ->
log.debug(
"Mapped TrainerRoad workout {}, target preview: {}",
trWorkoutId,
targetPreview(workout),
)
}
}

fun getActivities(username: String, startDate: LocalDate, endDate: LocalDate): List<Activity> {
val activities = trainerRoadApiClient.getActivities(username, startDate.toString(), endDate.toString())
fun getActivities(memberId: Long, startDate: LocalDate, endDate: LocalDate): List<Activity> {
val activityIds = trainerRoadApiClient.getTimeline(memberId, startDate.toString(), endDate.toString())
.activities
.filter {
val startedDate = it.started?.toLocalDate()
startedDate != null && startedDate in startDate..endDate
}
.map { it.id }

if (activityIds.isEmpty()) {
return emptyList()
}

val activities = trainerRoadApiClient.getActivities(memberId, activityIds.joinToString(","))
.filter { it.date.toLocalDate() in startDate..endDate }
val activityMapper = TrainerRoadActivityMapper()
return activities.map { mapToActivity(activityMapper, it) }
}

private fun mapToActivity(activityMapper: TrainerRoadActivityMapper, it: TrainerRoadActivityDTO): Activity {
val resource = trainerRoadApiClient.exportFit(it.completedRide!!.WorkoutRecordId.toString())
val activityId = it.completedRide?.WorkoutRecordId ?: it.activityId
val resource = trainerRoadApiClient.exportFit(activityId.toString())
return activityMapper.mapToActivity(it, resource)
}

private fun targetPreview(workout: Workout): String {
return workout.structure?.steps
?.filterIsInstance<SingleStep>()
?.take(8)
?.joinToString { "${it.name}:${it.target.start}-${it.target.end}" }
?: "no structure"
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package org.freekode.tp2intervals.infrastructure.platform.trainerroad

import com.fasterxml.jackson.annotation.JsonAlias

class TrainerRoadMemberDTO(
@JsonAlias("memberId")
val MemberId: Long,
@JsonAlias("username")
val Username: String?,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.freekode.tp2intervals.infrastructure.platform.trainerroad

import java.time.LocalDate
import java.time.LocalDateTime

class TrainerRoadTimelineDTO(
val plannedActivities: List<PlannedActivityDTO> = emptyList(),
val activities: List<ActivityStateDTO> = emptyList(),
) {
class PlannedActivityDTO(
val id: String,
val date: DateDTO,
val workoutId: Long?,
)

class ActivityStateDTO(
val id: Long,
val started: LocalDateTime?,
)

class DateDTO(
val year: Int,
val month: Int,
val day: Int,
) {
fun toLocalDate(): LocalDate = LocalDate.of(year, month, day)
}
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,52 @@
package org.freekode.tp2intervals.infrastructure.platform.trainerroad.activity

import com.fasterxml.jackson.annotation.JsonAlias
import com.fasterxml.jackson.annotation.JsonProperty
import java.time.LocalDateTime

class TrainerRoadActivityDTO(
@JsonAlias("id")
val Id: String,
@JsonProperty("Date")
@JsonAlias("date", "started")
val date: LocalDateTime,
@JsonProperty("CompletedRide")
@JsonAlias("completedRide")
val completedRide: CompletedRideDTO?,
@JsonProperty("Activity")
val activity: ActivityDTO?
@JsonAlias("activity")
val activity: ActivityDTO?,
@JsonAlias("activityId")
val activityId: Long?,
@JsonAlias("name")
val name: String?,
@JsonAlias("isOutside")
val isOutside: Boolean?,
@JsonAlias("activityType")
val activityType: Int?,
) {
class ActivityDTO(
@JsonProperty("Id")
@JsonAlias("id")
val id: String,
)

class CompletedRideDTO(
@JsonAlias("name")
val Name: String,
@JsonAlias("date")
val Date: LocalDateTime,
@JsonAlias("isOutside")
val IsOutside: Boolean,
@JsonAlias("tss")
val Tss: Int,
@JsonAlias("estimatedDuration")
val EstimatedDuration: Long,
@JsonAlias("duration")
val Duration: Long,
@JsonAlias("distance")
val Distance: Double,
@JsonAlias("workoutRecordId")
val WorkoutRecordId: Long
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ import org.springframework.core.io.Resource

class TrainerRoadActivityMapper {
fun mapToActivity(dto: TrainerRoadActivityDTO, resource: Resource): Activity {
val type = if (dto.completedRide!!.IsOutside) TrainingType.BIKE else TrainingType.VIRTUAL_BIKE
val completedRide = dto.completedRide
val type = if (completedRide?.IsOutside ?: (dto.isOutside == true)) {
TrainingType.BIKE
} else {
TrainingType.VIRTUAL_BIKE
}

return Activity(
dto.completedRide.Date,
completedRide?.Date ?: dto.date,
type,
dto.completedRide.Name,
completedRide?.Name ?: dto.name ?: "TrainerRoad activity",
Base64.encodeToString(resource)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class TrainerRoadActivityRepository(
}

override fun getActivities(startDate: LocalDate, endDate: LocalDate, types: List<TrainingType>): List<Activity> {
val username = trUsernameRepository.getUsername()
return trainerRoadApiClientService.getActivities(username, startDate, endDate)
val memberId = trUsernameRepository.getMemberId()
return trainerRoadApiClientService.getActivities(memberId, startDate, endDate)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,9 @@ class TRUsernameRepository(
fun getUsername(): String {
return trainerRoadMemberApiClient.getMember().Username!!
}

@Cacheable(cacheNames = ["trMemberIdCache"], key = "'singleton'")
fun getMemberId(): Long {
return trainerRoadMemberApiClient.getMember().MemberId
}
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,48 @@
package org.freekode.tp2intervals.infrastructure.platform.trainerroad.workout

import com.fasterxml.jackson.annotation.JsonAlias
import com.fasterxml.jackson.annotation.JsonProperty
import kotlin.math.roundToInt

class TRWorkoutResponseDTO(
@JsonProperty("Workout")
@JsonAlias("workout")
val workout: TRWorkout,
) {
class TRWorkout(
@JsonProperty("Details")
@JsonAlias("details")
val details: TrainerRoadWorkoutDetailsDTO,
@JsonProperty("IntervalData")
val intervalData: List<IntervalsDataDTO>,
@JsonAlias("intervalData")
val intervalData: List<IntervalsDataDTO> = emptyList(),
)

class IntervalsDataDTO(
@JsonProperty("Start")
@JsonAlias("start")
val start: Double,
@JsonProperty("End")
@JsonAlias("end")
val end: Double,
@JsonProperty("Name")
@JsonAlias("name")
val name: String,
@JsonProperty("IsFake")
@JsonAlias("isFake")
val isFake: Boolean,
@JsonProperty("TestInterval")
@JsonAlias("testInterval")
val testInterval: Boolean,
@JsonProperty("StartTargetPowerPercent")
val startTargetPowerPercent: Int,
)
@JsonAlias("startTargetPowerPercent")
val startTargetPowerPercent: Double = 0.0,
@JsonProperty("StartTarget")
@JsonAlias("startTarget")
val startTarget: List<Double>? = null,
) {
fun targetStart(): Int = (startTarget?.firstOrNull() ?: startTargetPowerPercent).roundToInt()

fun targetEnd(): Int = (startTarget?.getOrNull(1) ?: targetStart().toDouble()).roundToInt()
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
package org.freekode.tp2intervals.infrastructure.platform.trainerroad.workout

import com.fasterxml.jackson.annotation.JsonAlias
import com.fasterxml.jackson.annotation.JsonProperty

class TrainerRoadWorkoutDetailsDTO(
@JsonProperty("Id")
@JsonAlias("id")
val id: String,
@JsonProperty("WorkoutName")
@JsonAlias("workoutName")
val workoutName: String,
@JsonProperty("WorkoutDescription")
@JsonAlias("workoutDescription")
val workoutDescription: String,
@JsonProperty("IsOutside")
@JsonAlias("isOutside")
val isOutside: Boolean,
@JsonProperty("Tss")
@JsonAlias("tss")
val tss: Int,
@JsonProperty("Duration")
@JsonAlias("duration")
val duration: Int,
)
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ class TrainerRoadWorkoutMapper {
return Workout(
toWorkoutDetails(trWorkout.details, removeHtmlTags),
null,
WorkoutStructure(WorkoutStructure.TargetUnit.FTP_PERCENTAGE, steps),
steps.takeIf { it.isNotEmpty() }
?.let { WorkoutStructure(WorkoutStructure.TargetUnit.FTP_PERCENTAGE, it) },
)
}

Expand All @@ -37,12 +38,10 @@ class TrainerRoadWorkoutMapper {
continue
}
val stepLength = StepLength.seconds((interval.end - interval.start).toLong())
val ftpPercent = interval.startTargetPowerPercent

val name = if (interval.name == "Fake") "Step" else interval.name

val singleStep =
SingleStep(name, stepLength, StepTarget(ftpPercent, ftpPercent), null, false)
SingleStep(name, stepLength, StepTarget(interval.targetStart(), interval.targetEnd()), null, false)
steps.add(singleStep)
}
return steps
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ class TrainerRoadWorkoutRepository(
}

override fun getWorkoutsFromCalendar(startDate: LocalDate, endDate: LocalDate): List<Workout> {
val username = trUsernameRepository.getUsername()
return trainerRoadApiClientService.getWorkoutsFromCalendar(startDate, endDate, username)
val memberId = trUsernameRepository.getMemberId()
return trainerRoadApiClientService.getWorkoutsFromCalendar(startDate, endDate, memberId)
}

override fun saveWorkoutsToCalendar(workouts: List<Workout>) {
Expand Down
Loading