Skip to main content
Glama
planning.ts5.34 kB
import { addDays, format } from 'date-fns'; import { IntervalsClient } from '../clients/intervals.js'; import { TrainerRoadClient } from '../clients/trainerroad.js'; import { parseDateString, getToday } from '../utils/date-parser.js'; import type { PlannedWorkout } from '../types/index.js'; import type { GetUpcomingWorkoutsInput, GetPlannedWorkoutDetailsInput, } from './types.js'; export class PlanningTools { constructor( private intervals: IntervalsClient, private trainerroad: TrainerRoadClient | null ) {} /** * Get upcoming planned workouts from both calendars */ async getUpcomingWorkouts( params: GetUpcomingWorkoutsInput ): Promise<{ trainerroad: PlannedWorkout[]; intervals: PlannedWorkout[]; merged: PlannedWorkout[]; }> { const { days, sport } = params; const today = new Date(); const endDate = addDays(today, days); const startDateStr = format(today, 'yyyy-MM-dd'); const endDateStr = format(endDate, 'yyyy-MM-dd'); // Fetch from both sources in parallel const [trainerroadWorkouts, intervalsWorkouts] = await Promise.all([ this.trainerroad?.getPlannedWorkouts(startDateStr, endDateStr).catch((e) => { console.error('Error fetching TrainerRoad workouts:', e); return []; }) ?? Promise.resolve([]), this.intervals.getPlannedEvents(startDateStr, endDateStr).catch((e) => { console.error('Error fetching Intervals.icu events:', e); return []; }), ]); // Merge and deduplicate const merged = this.mergeWorkouts(trainerroadWorkouts, intervalsWorkouts); // Sort by date const sortByDate = (a: PlannedWorkout, b: PlannedWorkout) => new Date(a.date).getTime() - new Date(b.date).getTime(); return { trainerroad: trainerroadWorkouts.sort(sortByDate), intervals: intervalsWorkouts.sort(sortByDate), merged: merged.sort(sortByDate), }; } /** * Get detailed information about a specific planned workout */ async getPlannedWorkoutDetails( params: GetPlannedWorkoutDetailsInput ): Promise<PlannedWorkout | null> { const { workout_id, date, source } = params; if (workout_id) { // Find by ID if (source === 'trainerroad' && this.trainerroad) { const workouts = await this.trainerroad.getUpcomingWorkouts(30); return workouts.find((w) => w.id === workout_id) ?? null; } else if (source === 'intervals.icu') { const today = format(new Date(), 'yyyy-MM-dd'); const futureDate = format(addDays(new Date(), 30), 'yyyy-MM-dd'); const workouts = await this.intervals.getPlannedEvents(today, futureDate); return workouts.find((w) => w.id === workout_id) ?? null; } // Search both if source not specified const [trWorkouts, intWorkouts] = await Promise.all([ this.trainerroad?.getUpcomingWorkouts(30) ?? Promise.resolve([]), this.intervals .getPlannedEvents( format(new Date(), 'yyyy-MM-dd'), format(addDays(new Date(), 30), 'yyyy-MM-dd') ) .catch(() => []), ]); return ( trWorkouts.find((w) => w.id === workout_id) ?? intWorkouts.find((w) => w.id === workout_id) ?? null ); } if (date) { // Find by date const dateStr = parseDateString(date); if (source === 'trainerroad' && this.trainerroad) { const workouts = await this.trainerroad.getPlannedWorkouts(dateStr, dateStr); return workouts[0] ?? null; } else if (source === 'intervals.icu') { const workouts = await this.intervals.getPlannedEvents(dateStr, dateStr); return workouts[0] ?? null; } // Get from both and return first match const [trWorkouts, intWorkouts] = await Promise.all([ this.trainerroad?.getPlannedWorkouts(dateStr, dateStr) ?? Promise.resolve([]), this.intervals.getPlannedEvents(dateStr, dateStr).catch(() => []), ]); // Prefer TrainerRoad (more detailed) return trWorkouts[0] ?? intWorkouts[0] ?? null; } return null; } /** * Merge workouts from both sources, avoiding duplicates */ private mergeWorkouts( trainerroad: PlannedWorkout[], intervals: PlannedWorkout[] ): PlannedWorkout[] { const merged = [...trainerroad]; for (const intervalsWorkout of intervals) { const isDuplicate = trainerroad.some((tr) => this.areWorkoutsSimilar(tr, intervalsWorkout) ); if (!isDuplicate) { merged.push(intervalsWorkout); } } return merged; } /** * Check if two workouts are likely the same */ private areWorkoutsSimilar(a: PlannedWorkout, b: PlannedWorkout): boolean { // Same day check const dateA = a.date.split('T')[0]; const dateB = b.date.split('T')[0]; if (dateA !== dateB) return false; // Similar name check (fuzzy) const nameA = a.name.toLowerCase().replace(/[^a-z0-9]/g, ''); const nameB = b.name.toLowerCase().replace(/[^a-z0-9]/g, ''); if (nameA.includes(nameB) || nameB.includes(nameA)) return true; // Similar TSS check if (a.expected_tss && b.expected_tss) { const tssDiff = Math.abs(a.expected_tss - b.expected_tss); if (tssDiff < 5) return true; } return false; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/gesteves/domestique'

If you have feedback or need assistance with the MCP directory API, please join our Discord server