ts-trainingpeaks 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/http.ts ADDED
@@ -0,0 +1,265 @@
1
+ /**
2
+ * HTTP Client for TrainingPeaks API
3
+ * Handles cookies, authentication, and API requests
4
+ */
5
+
6
+ import type { TPCookie } from './types'
7
+
8
+ export interface HttpClientOptions {
9
+ baseUrl?: string
10
+ timeout?: number
11
+ userAgent?: string
12
+ }
13
+
14
+ export class HttpClient {
15
+ private cookies: Map<string, TPCookie> = new Map()
16
+ private baseUrl: string
17
+ private timeout: number
18
+ private userAgent: string
19
+
20
+ constructor(options: HttpClientOptions = {}) {
21
+ this.baseUrl = options.baseUrl || 'https://home.trainingpeaks.com'
22
+ this.timeout = options.timeout || 30000
23
+ this.userAgent = options.userAgent || 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
24
+ }
25
+
26
+ setCookies(cookies: TPCookie[]): void {
27
+ for (const cookie of cookies) {
28
+ const key = `${cookie.domain || ''}:${cookie.name}`
29
+ this.cookies.set(key, cookie)
30
+ }
31
+ }
32
+
33
+ getCookies(): TPCookie[] {
34
+ return Array.from(this.cookies.values())
35
+ }
36
+
37
+ clearCookies(): void {
38
+ this.cookies.clear()
39
+ }
40
+
41
+ private getCookieHeader(url: string): string {
42
+ const parsed = new URL(url)
43
+ const matching: TPCookie[] = []
44
+
45
+ for (const cookie of this.cookies.values()) {
46
+ // Check domain match
47
+ if (cookie.domain) {
48
+ const domain = cookie.domain.startsWith('.') ? cookie.domain.slice(1) : cookie.domain
49
+ if (!parsed.hostname.endsWith(domain) && parsed.hostname !== domain) {
50
+ continue
51
+ }
52
+ }
53
+
54
+ // Check path match
55
+ if (cookie.path && !parsed.pathname.startsWith(cookie.path)) {
56
+ continue
57
+ }
58
+
59
+ // Check secure
60
+ if (cookie.secure && parsed.protocol !== 'https:') {
61
+ continue
62
+ }
63
+
64
+ // Check expiration
65
+ if (cookie.expires && cookie.expires < new Date()) {
66
+ continue
67
+ }
68
+
69
+ matching.push(cookie)
70
+ }
71
+
72
+ return matching.map(c => `${c.name}=${c.value}`).join('; ')
73
+ }
74
+
75
+ private extractCookies(response: Response, url: string): void {
76
+ const setCookies = response.headers.getSetCookie()
77
+ const parsedUrl = new URL(url)
78
+
79
+ for (const cookieStr of setCookies) {
80
+ const cookie = this.parseCookie(cookieStr, parsedUrl.hostname)
81
+ if (cookie) {
82
+ const key = `${cookie.domain || ''}:${cookie.name}`
83
+ this.cookies.set(key, cookie)
84
+ }
85
+ }
86
+ }
87
+
88
+ private parseCookie(cookieStr: string, defaultDomain: string): TPCookie | null {
89
+ const parts = cookieStr.split(';').map(p => p.trim())
90
+ const [nameValue, ...attrs] = parts
91
+
92
+ if (!nameValue || !nameValue.includes('=')) return null
93
+
94
+ const [name, ...valueParts] = nameValue.split('=')
95
+ const value = valueParts.join('=')
96
+
97
+ const cookie: TPCookie = {
98
+ name: name.trim(),
99
+ value: value.trim(),
100
+ domain: defaultDomain,
101
+ path: '/',
102
+ }
103
+
104
+ for (const attr of attrs) {
105
+ const [key, ...attrValueParts] = attr.split('=')
106
+ const attrKey = key.trim().toLowerCase()
107
+ const attrValue = attrValueParts.join('=').trim()
108
+
109
+ switch (attrKey) {
110
+ case 'domain':
111
+ cookie.domain = attrValue
112
+ break
113
+ case 'path':
114
+ cookie.path = attrValue
115
+ break
116
+ case 'expires':
117
+ cookie.expires = new Date(attrValue)
118
+ break
119
+ case 'max-age':
120
+ const maxAge = parseInt(attrValue, 10)
121
+ if (maxAge) cookie.expires = new Date(Date.now() + maxAge * 1000)
122
+ break
123
+ case 'secure':
124
+ cookie.secure = true
125
+ break
126
+ case 'httponly':
127
+ cookie.httpOnly = true
128
+ break
129
+ }
130
+ }
131
+
132
+ return cookie
133
+ }
134
+
135
+ async request<T>(
136
+ method: string,
137
+ path: string,
138
+ options: {
139
+ body?: unknown
140
+ headers?: Record<string, string>
141
+ params?: Record<string, string | number | boolean | undefined>
142
+ } = {}
143
+ ): Promise<T> {
144
+ let url = path.startsWith('http') ? path : `${this.baseUrl}${path}`
145
+
146
+ // Add query params
147
+ if (options.params) {
148
+ const searchParams = new URLSearchParams()
149
+ for (const [key, value] of Object.entries(options.params)) {
150
+ if (value !== undefined) {
151
+ searchParams.set(key, String(value))
152
+ }
153
+ }
154
+ const queryString = searchParams.toString()
155
+ if (queryString) {
156
+ url += (url.includes('?') ? '&' : '?') + queryString
157
+ }
158
+ }
159
+
160
+ const headers: Record<string, string> = {
161
+ 'User-Agent': this.userAgent,
162
+ 'Accept': 'application/json, text/plain, */*',
163
+ 'Accept-Language': 'en-US,en;q=0.9',
164
+ ...options.headers,
165
+ }
166
+
167
+ // Add cookies
168
+ const cookieHeader = this.getCookieHeader(url)
169
+ if (cookieHeader) {
170
+ headers['Cookie'] = cookieHeader
171
+ }
172
+
173
+ // Add body
174
+ let body: string | undefined
175
+ if (options.body !== undefined) {
176
+ if (typeof options.body === 'object') {
177
+ headers['Content-Type'] = headers['Content-Type'] || 'application/json'
178
+ body = JSON.stringify(options.body)
179
+ }
180
+ else if (typeof options.body === 'string') {
181
+ body = options.body
182
+ }
183
+ }
184
+
185
+ const controller = new AbortController()
186
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout)
187
+
188
+ try {
189
+ const response = await fetch(url, {
190
+ method,
191
+ headers,
192
+ body,
193
+ signal: controller.signal,
194
+ redirect: 'follow',
195
+ })
196
+
197
+ clearTimeout(timeoutId)
198
+
199
+ // Extract cookies from response
200
+ this.extractCookies(response, url)
201
+
202
+ if (!response.ok) {
203
+ const text = await response.text()
204
+ throw new Error(`HTTP ${response.status}: ${text}`)
205
+ }
206
+
207
+ const contentType = response.headers.get('content-type')
208
+ if (contentType?.includes('application/json')) {
209
+ return await response.json() as T
210
+ }
211
+
212
+ return await response.text() as T
213
+ }
214
+ catch (error) {
215
+ clearTimeout(timeoutId)
216
+ if (error instanceof Error && error.name === 'AbortError') {
217
+ throw new Error(`Request timeout: ${url}`)
218
+ }
219
+ throw error
220
+ }
221
+ }
222
+
223
+ async get<T>(path: string, params?: Record<string, string | number | boolean | undefined>): Promise<T> {
224
+ return this.request<T>('GET', path, { params })
225
+ }
226
+
227
+ async post<T>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T> {
228
+ return this.request<T>('POST', path, { body, headers })
229
+ }
230
+
231
+ async put<T>(path: string, body?: unknown): Promise<T> {
232
+ return this.request<T>('PUT', path, { body })
233
+ }
234
+
235
+ async delete<T>(path: string): Promise<T> {
236
+ return this.request<T>('DELETE', path)
237
+ }
238
+
239
+ // Save cookies to disk
240
+ async saveCookies(path: string): Promise<void> {
241
+ const cookies = this.getCookies()
242
+ await Bun.write(path, JSON.stringify(cookies, null, 2))
243
+ }
244
+
245
+ // Load cookies from disk
246
+ async loadCookies(path: string): Promise<void> {
247
+ try {
248
+ const file = Bun.file(path)
249
+ const content = await file.text()
250
+ const cookies = JSON.parse(content) as TPCookie[]
251
+
252
+ // Convert date strings back to Date objects
253
+ for (const cookie of cookies) {
254
+ if (cookie.expires) {
255
+ cookie.expires = new Date(cookie.expires)
256
+ }
257
+ }
258
+
259
+ this.setCookies(cookies)
260
+ }
261
+ catch {
262
+ // File doesn't exist or is invalid, ignore
263
+ }
264
+ }
265
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { TrainingPeaks, default } from './TrainingPeaks'
2
+ export type { TrainingPeaksConfig } from './TrainingPeaks'
3
+ export * from './types'
4
+ export { Browser } from './browser'
5
+ export { HttpClient } from './http'
package/src/types.ts ADDED
@@ -0,0 +1,319 @@
1
+ export interface TPCredentials {
2
+ username: string
3
+ password: string
4
+ }
5
+
6
+ export interface TPTokens {
7
+ accessToken: string
8
+ refreshToken?: string
9
+ expiresAt?: number
10
+ userId?: number
11
+ accountType?: 'athlete' | 'coach'
12
+ }
13
+
14
+ export interface TPCookie {
15
+ name: string
16
+ value: string
17
+ domain?: string
18
+ path?: string
19
+ expires?: Date
20
+ secure?: boolean
21
+ httpOnly?: boolean
22
+ }
23
+
24
+ export interface TPAthlete {
25
+ Id: number
26
+ UserId?: number
27
+ FirstName: string
28
+ LastName: string
29
+ Email?: string
30
+ DateOfBirth?: string
31
+ Gender?: string
32
+ City?: string
33
+ State?: string
34
+ Country?: string
35
+ Weight?: number
36
+ ProfilePhotoUrl?: string
37
+ IsCoach?: boolean
38
+ IsPremium?: boolean
39
+ ThresholdPower?: number
40
+ ThresholdHeartRate?: number
41
+ ThresholdPace?: number
42
+ }
43
+
44
+ export interface TPCoachAthlete extends TPAthlete {
45
+ AthleteId: number
46
+ CoachId: number
47
+ Status: string
48
+ LastActivity?: string
49
+ CompliancePercent?: number
50
+ }
51
+
52
+ export interface TPAthleteGroup {
53
+ id: number
54
+ coachId: number
55
+ name: string
56
+ athleteIds: number[]
57
+ isDefault: boolean
58
+ }
59
+
60
+ export interface TPWorkout {
61
+ workoutId: number
62
+ athleteId: number
63
+ workoutDay: string
64
+ title: string
65
+ description?: string
66
+ coachComments?: string
67
+ workoutComments?: unknown[]
68
+ workoutTypeValueId: number
69
+ completed: boolean | null
70
+ // Planned metrics
71
+ totalTimePlanned?: number
72
+ distancePlanned?: number
73
+ tssPlanned?: number
74
+ ifPlanned?: number
75
+ velocityPlanned?: number
76
+ caloriesPlanned?: number
77
+ energyPlanned?: number
78
+ elevationGainPlanned?: number
79
+ // Actual metrics
80
+ totalTime?: number
81
+ distance?: number
82
+ energy?: number
83
+ calories?: number
84
+ // Heart rate
85
+ heartRateAverage?: number
86
+ heartRateMinimum?: number
87
+ heartRateMaximum?: number
88
+ // Power
89
+ powerAverage?: number
90
+ powerMaximum?: number
91
+ normalizedPowerActual?: number
92
+ normalizedSpeedActual?: number
93
+ if?: number
94
+ tssActual?: number
95
+ // Cadence
96
+ cadenceAverage?: number
97
+ cadenceMaximum?: number
98
+ // Speed
99
+ velocityAverage?: number
100
+ velocityMaximum?: number
101
+ // Elevation
102
+ elevationGain?: number
103
+ elevationLoss?: number
104
+ elevationMinimum?: number
105
+ elevationAverage?: number
106
+ elevationMaximum?: number
107
+ // Temperature
108
+ tempMin?: number
109
+ tempAvg?: number
110
+ tempMax?: number
111
+ // RPE/Feeling
112
+ rpe?: number
113
+ feeling?: number
114
+ // Compliance
115
+ complianceDurationPercent?: number
116
+ complianceDistancePercent?: number
117
+ complianceTssPercent?: number
118
+ // Other
119
+ structure?: TPWorkoutStructureResponse
120
+ userTags?: string[] | null
121
+ syncedTo?: string[]
122
+ orderOnDay?: number
123
+ personalRecordCount?: number
124
+ lastModifiedDate?: string
125
+ isLocked?: boolean | null
126
+ startTime?: string | null
127
+ startTimePlanned?: string | null
128
+ sharedWorkoutInformationKey?: string
129
+ publicSettingValue?: number
130
+ code?: string | null
131
+ equipmentBikeId?: number | null
132
+ equipmentShoeId?: number | null
133
+ poolLengthOptionId?: number | null
134
+ workoutSubTypeId?: number | null
135
+ workoutDeviceSource?: string | null
136
+ }
137
+
138
+ export interface TPWorkoutStructureResponse {
139
+ structure: TPWorkoutStructureStep[]
140
+ polyline?: number[][]
141
+ primaryLengthMetric?: string
142
+ primaryIntensityMetric?: string
143
+ primaryIntensityTargetOrRange?: string
144
+ }
145
+
146
+ export interface TPWorkoutStructureStep {
147
+ type: 'step' | 'repetition'
148
+ name?: string
149
+ length: {
150
+ value: number
151
+ unit: string
152
+ }
153
+ steps?: TPWorkoutStructureStep[]
154
+ targets?: {
155
+ minValue?: number
156
+ maxValue?: number
157
+ }[]
158
+ intensityClass?: string
159
+ openDuration?: boolean
160
+ begin?: number
161
+ end?: number
162
+ }
163
+
164
+ // Workout type IDs returned by API
165
+ export const TPWorkoutTypeIds = {
166
+ Swim: 1,
167
+ Bike: 2,
168
+ Run: 3,
169
+ Brick: 4,
170
+ XTrain: 5,
171
+ Strength: 6,
172
+ Custom: 7,
173
+ Walk: 8,
174
+ Hike: 9,
175
+ Row: 10,
176
+ Ski: 11,
177
+ Other: 100,
178
+ } as const
179
+
180
+ export type TPWorkoutTypeId = typeof TPWorkoutTypeIds[keyof typeof TPWorkoutTypeIds]
181
+
182
+ export type TPWorkoutType =
183
+ | 'Swim'
184
+ | 'Bike'
185
+ | 'Run'
186
+ | 'Brick'
187
+ | 'XTrain'
188
+ | 'Strength'
189
+ | 'Custom'
190
+ | 'Walk'
191
+ | 'Hike'
192
+ | 'Row'
193
+ | 'Ski'
194
+ | 'Other'
195
+
196
+ export function getWorkoutTypeName(typeId: number): TPWorkoutType {
197
+ const map: Record<number, TPWorkoutType> = {
198
+ 1: 'Swim',
199
+ 2: 'Bike',
200
+ 3: 'Run',
201
+ 4: 'Brick',
202
+ 5: 'XTrain',
203
+ 6: 'Strength',
204
+ 7: 'Custom',
205
+ 8: 'Walk',
206
+ 9: 'Hike',
207
+ 10: 'Row',
208
+ 11: 'Ski',
209
+ }
210
+ return map[typeId] || 'Other'
211
+ }
212
+
213
+ export interface TPDailySummary {
214
+ Date: string
215
+ TssTotal?: number
216
+ DurationTotal?: number
217
+ DistanceTotal?: number
218
+ CaloriesTotal?: number
219
+ WorkoutsPlanned?: number
220
+ WorkoutsCompleted?: number
221
+ CompliancePercent?: number
222
+ }
223
+
224
+ export interface TPMetrics {
225
+ Date: string
226
+ Weight?: number
227
+ RestingHeartRate?: number
228
+ HrvRmssd?: number
229
+ SleepHours?: number
230
+ SleepQuality?: number
231
+ Fatigue?: number
232
+ Mood?: number
233
+ Motivation?: number
234
+ Stress?: number
235
+ Soreness?: number
236
+ Injury?: number
237
+ Steps?: number
238
+ CaloriesConsumed?: number
239
+ Comments?: string
240
+ }
241
+
242
+ export interface TPCalendarDay {
243
+ Date: string
244
+ Workouts: TPWorkout[]
245
+ Metrics?: TPMetrics
246
+ Notes?: string[]
247
+ }
248
+
249
+ export interface TPPerformanceChart {
250
+ StartDate: string
251
+ EndDate: string
252
+ Ctl: number[] // Chronic Training Load (fitness)
253
+ Atl: number[] // Acute Training Load (fatigue)
254
+ Tsb: number[] // Training Stress Balance (form)
255
+ Tss: number[] // Training Stress Score
256
+ }
257
+
258
+ export interface TPActivityFile {
259
+ Id: number
260
+ FileName: string
261
+ FileType: string
262
+ UploadDate: string
263
+ ProcessedDate?: string
264
+ Status: string
265
+ WorkoutId?: number
266
+ }
267
+
268
+ export interface TPUploadResult {
269
+ Success: boolean
270
+ FileId?: number
271
+ WorkoutId?: number
272
+ Message?: string
273
+ Errors?: string[]
274
+ }
275
+
276
+ export interface TPSearchParams {
277
+ startDate?: Date
278
+ endDate?: Date
279
+ workoutTypes?: TPWorkoutType[]
280
+ completed?: boolean
281
+ limit?: number
282
+ offset?: number
283
+ }
284
+
285
+ export interface TPLibraryWorkout {
286
+ Id: number
287
+ Title: string
288
+ Description?: string
289
+ WorkoutType: TPWorkoutType
290
+ Duration?: number
291
+ Distance?: number
292
+ Tss?: number
293
+ Structure?: TPWorkoutStructureResponse
294
+ Tags?: string[]
295
+ IsPublic?: boolean
296
+ CreatedDate?: string
297
+ ModifiedDate?: string
298
+ }
299
+
300
+ export interface TPPlan {
301
+ Id: number
302
+ Name: string
303
+ Description?: string
304
+ StartDate?: string
305
+ EndDate?: string
306
+ AthleteId?: number
307
+ CoachId?: number
308
+ Status: string
309
+ Weeks?: TPPlanWeek[]
310
+ }
311
+
312
+ export interface TPPlanWeek {
313
+ WeekNumber: number
314
+ StartDate: string
315
+ EndDate: string
316
+ Description?: string
317
+ PlannedTss?: number
318
+ PlannedDuration?: number
319
+ }