trainerroad-cli 0.1.1 → 0.2.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.
@@ -12,6 +12,18 @@ function ensureLeadingSlash(value) {
12
12
  return value;
13
13
  }
14
14
 
15
+ function toApiDateOnly(value) {
16
+ const match = String(value ?? "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
17
+ if (!match) {
18
+ throw new Error(`Invalid date "${value}". Expected YYYY-MM-DD.`);
19
+ }
20
+ return {
21
+ year: Number(match[1]),
22
+ month: Number(match[2]),
23
+ day: Number(match[3]),
24
+ };
25
+ }
26
+
15
27
  function plannedDateToIso(item) {
16
28
  const year = String(item.date?.year ?? "").padStart(4, "0");
17
29
  const month = String(item.date?.month ?? "").padStart(2, "0");
@@ -445,6 +457,288 @@ export class TrainerRoadClient {
445
457
  return results;
446
458
  }
447
459
 
460
+ async getWorkoutProfilesByZone(usernameForReferer = null) {
461
+ return this.#requestJson("/app/api/workouts/workout-profiles-by-zone", {
462
+ headers: {
463
+ "trainerroad-jsonformat": "camel-case",
464
+ "tr-cache-control": "use-cache",
465
+ referer: `${APP_URL}/workouts/list`,
466
+ ...(usernameForReferer ? { "x-trainerroad-username": usernameForReferer } : {}),
467
+ },
468
+ });
469
+ }
470
+
471
+ async searchWorkoutLibrary(predicate, usernameForReferer = null) {
472
+ return this.#requestJson("/app/api/workouts", {
473
+ method: "POST",
474
+ headers: {
475
+ "content-type": "application/json",
476
+ "trainerroad-jsonformat": "camel-case",
477
+ "tr-cache-control": "no-cache",
478
+ referer: `${APP_URL}/workouts/list`,
479
+ ...(usernameForReferer ? { "x-trainerroad-username": usernameForReferer } : {}),
480
+ },
481
+ body: JSON.stringify(predicate),
482
+ });
483
+ }
484
+
485
+ async getWorkoutsByIds(workoutIds, usernameForReferer = null) {
486
+ return this.#requestJson("/app/api/workouts/by-id", {
487
+ method: "POST",
488
+ headers: {
489
+ "content-type": "application/json",
490
+ "trainerroad-jsonformat": "camel-case",
491
+ "tr-cache-control": "use-cache",
492
+ referer: `${APP_URL}/workouts/list`,
493
+ ...(usernameForReferer ? { "x-trainerroad-username": usernameForReferer } : {}),
494
+ },
495
+ body: JSON.stringify(workoutIds),
496
+ });
497
+ }
498
+
499
+ async getWorkoutSummary(workoutId, usernameForReferer = null) {
500
+ return this.#requestJson(`/app/api/workouts/${encodeURIComponent(workoutId)}/summary`, {
501
+ headers: {
502
+ "trainerroad-jsonformat": "camel-case",
503
+ "tr-cache-control": "use-cache",
504
+ referer: `${APP_URL}/workouts/list`,
505
+ ...(usernameForReferer ? { "x-trainerroad-username": usernameForReferer } : {}),
506
+ },
507
+ });
508
+ }
509
+
510
+ async getWorkoutLevels(workoutId, usernameForReferer = null) {
511
+ return this.#requestJson(`/app/api/workouts/${encodeURIComponent(workoutId)}/levels`, {
512
+ headers: {
513
+ "trainerroad-jsonformat": "camel-case",
514
+ "tr-cache-control": "use-cache",
515
+ referer: `${APP_URL}/workouts/list`,
516
+ ...(usernameForReferer ? { "x-trainerroad-username": usernameForReferer } : {}),
517
+ },
518
+ });
519
+ }
520
+
521
+ async getWorkoutChartData(workoutId, usernameForReferer = null) {
522
+ return this.#requestJson(`/app/api/workouts/${encodeURIComponent(workoutId)}/chart-data`, {
523
+ headers: {
524
+ "trainerroad-jsonformat": "camel-case",
525
+ "tr-cache-control": "use-cache",
526
+ referer: `${APP_URL}/workouts/list`,
527
+ ...(usernameForReferer ? { "x-trainerroad-username": usernameForReferer } : {}),
528
+ },
529
+ });
530
+ }
531
+
532
+ async getWorkoutInformation(workoutIds, usernameForReferer = null) {
533
+ const ids = (Array.isArray(workoutIds) ? workoutIds : [])
534
+ .map((value) => Number(value))
535
+ .filter((value) => Number.isFinite(value));
536
+ const params = new URLSearchParams({ ids: ids.join(",") });
537
+ return this.#requestJson(`/app/api/workout-information?${params.toString()}`, {
538
+ headers: {
539
+ "trainerroad-jsonformat": "camel-case",
540
+ "tr-cache-control": "use-cache",
541
+ referer: `${APP_URL}/cycling/train-now`,
542
+ ...(usernameForReferer ? { "x-trainerroad-username": usernameForReferer } : {}),
543
+ },
544
+ });
545
+ }
546
+
547
+ async getTrainNowStatus(usernameForReferer = null) {
548
+ return this.#requestJson("/app/api/train-now", {
549
+ headers: {
550
+ "trainerroad-jsonformat": "camel-case",
551
+ "tr-cache-control": "use-cache",
552
+ referer: `${APP_URL}/cycling/train-now`,
553
+ ...(usernameForReferer ? { "x-trainerroad-username": usernameForReferer } : {}),
554
+ },
555
+ });
556
+ }
557
+
558
+ async getTrainNowSuggestions({ duration, numSuggestions = 10 } = {}, usernameForReferer = null) {
559
+ return this.#requestJson("/app/api/train-now", {
560
+ method: "POST",
561
+ headers: {
562
+ "content-type": "application/json",
563
+ "trainerroad-jsonformat": "camel-case",
564
+ "tr-cache-control": "no-cache",
565
+ referer: `${APP_URL}/cycling/train-now`,
566
+ ...(usernameForReferer ? { "x-trainerroad-username": usernameForReferer } : {}),
567
+ },
568
+ body: JSON.stringify({ duration, numSuggestions }),
569
+ });
570
+ }
571
+
572
+ async tryAddWorkoutToCalendar(
573
+ workoutId,
574
+ dateIso,
575
+ { timeOfDay = null, isManualComplete = false, outside = false, usernameForReferer } = {},
576
+ ) {
577
+ const attempts = [
578
+ {
579
+ endpoint: "react-calendar",
580
+ path: "/app/api/react-calendar/planned-tr-workout",
581
+ body: {
582
+ date: toApiDateOnly(dateIso),
583
+ time: timeOfDay,
584
+ workoutId: Number(workoutId),
585
+ type: outside ? 1 : 0,
586
+ recommendationReason: null,
587
+ },
588
+ },
589
+ {
590
+ endpoint: "legacy-plannedactivities",
591
+ path: "/app/api/calendar/plannedactivities/workout",
592
+ body: {
593
+ id: Number(workoutId),
594
+ date: dateIso,
595
+ timeOfDay,
596
+ isManualComplete: Boolean(isManualComplete),
597
+ recommendationReason: null,
598
+ },
599
+ },
600
+ ];
601
+
602
+ const results = [];
603
+ for (const attempt of attempts) {
604
+ const response = await this.#request(attempt.path, {
605
+ method: "POST",
606
+ headers: {
607
+ "content-type": "application/json",
608
+ "trainerroad-jsonformat": "camel-case",
609
+ "tr-cache-control": "no-cache",
610
+ referer: `${APP_URL}/calendar/${usernameForReferer}`,
611
+ },
612
+ body: JSON.stringify(attempt.body),
613
+ });
614
+ const text = await response.text();
615
+ let payload;
616
+ try {
617
+ payload = JSON.parse(text);
618
+ } catch {
619
+ payload = text;
620
+ }
621
+ results.push({
622
+ endpoint: attempt.endpoint,
623
+ status: response.status,
624
+ ok: response.ok,
625
+ requestBody: attempt.body,
626
+ response: payload,
627
+ });
628
+ if (response.ok) break;
629
+ }
630
+ return results;
631
+ }
632
+
633
+ async copyPlannedActivity(plannedActivityId, dateIso, usernameForReferer) {
634
+ const response = await this.#request(
635
+ `/app/api/calendar/plannedactivities/${encodeURIComponent(plannedActivityId)}/copy/${encodeURIComponent(dateIso)}`,
636
+ {
637
+ method: "POST",
638
+ headers: {
639
+ "trainerroad-jsonformat": "camel-case",
640
+ "tr-cache-control": "no-cache",
641
+ referer: `${APP_URL}/calendar/${usernameForReferer}`,
642
+ },
643
+ },
644
+ );
645
+
646
+ if (!response.ok) {
647
+ const text = await response.text();
648
+ throw new Error(
649
+ `Request failed: ${response.status} ${response.statusText} for copy planned activity -> ${text}`,
650
+ );
651
+ }
652
+
653
+ const text = await response.text();
654
+ if (!text) {
655
+ return { ok: true, status: response.status, empty: true };
656
+ }
657
+
658
+ try {
659
+ return JSON.parse(text);
660
+ } catch {
661
+ return { ok: true, status: response.status, raw: text };
662
+ }
663
+ }
664
+
665
+ async getPlannedActivity(plannedActivityId, usernameForReferer) {
666
+ return this.#requestJson(`/app/api/calendar/plannedactivities/${encodeURIComponent(plannedActivityId)}`, {
667
+ headers: {
668
+ "trainerroad-jsonformat": "camel-case",
669
+ "tr-cache-control": "use-cache",
670
+ referer: `${APP_URL}/calendar/${usernameForReferer}`,
671
+ },
672
+ });
673
+ }
674
+
675
+ async getPlannedActivityAlternates(plannedActivityId, category, usernameForReferer) {
676
+ return this.#requestJson(
677
+ `/app/api/calendar/plannedactivities/${encodeURIComponent(plannedActivityId)}/alternates/${encodeURIComponent(category)}`,
678
+ {
679
+ headers: {
680
+ "trainerroad-jsonformat": "camel-case",
681
+ "tr-cache-control": "use-cache",
682
+ referer: `${APP_URL}/calendar/${usernameForReferer}`,
683
+ },
684
+ },
685
+ );
686
+ }
687
+
688
+ async movePlannedActivity(plannedActivityId, newDateIso, usernameForReferer) {
689
+ return this.#requestJson(`/app/api/react-calendar/planned-activity/${encodeURIComponent(plannedActivityId)}/move`, {
690
+ method: "PUT",
691
+ headers: {
692
+ "content-type": "application/json",
693
+ "trainerroad-jsonformat": "camel-case",
694
+ "tr-cache-control": "no-cache",
695
+ referer: `${APP_URL}/calendar/${usernameForReferer}`,
696
+ },
697
+ body: JSON.stringify({ newDate: toApiDateOnly(newDateIso) }),
698
+ });
699
+ }
700
+
701
+ async replacePlannedActivityWithAlternate(
702
+ plannedActivityId,
703
+ alternateWorkoutId,
704
+ { updateDuration = false, usernameForReferer } = {},
705
+ ) {
706
+ return this.#requestJson(
707
+ `/app/api/react-calendar/planned-activity/${encodeURIComponent(plannedActivityId)}/replace-with-alternate`,
708
+ {
709
+ method: "PUT",
710
+ headers: {
711
+ "content-type": "application/json",
712
+ "trainerroad-jsonformat": "camel-case",
713
+ "tr-cache-control": "no-cache",
714
+ referer: `${APP_URL}/calendar/${usernameForReferer}`,
715
+ },
716
+ body: JSON.stringify({
717
+ alternateWorkoutId: Number(alternateWorkoutId),
718
+ updateDuration: Boolean(updateDuration),
719
+ }),
720
+ },
721
+ );
722
+ }
723
+
724
+ async switchPlannedActivityMode(plannedActivityId, mode, usernameForReferer) {
725
+ const normalizedMode = String(mode ?? "").trim().toLowerCase();
726
+ if (!["inside", "outside"].includes(normalizedMode)) {
727
+ throw new Error(`Invalid mode "${mode}". Expected "inside" or "outside".`);
728
+ }
729
+ return this.#requestJson(
730
+ `/app/api/react-calendar/planned-activity/${encodeURIComponent(plannedActivityId)}/switch-to-${normalizedMode}`,
731
+ {
732
+ method: "PUT",
733
+ headers: {
734
+ "trainerroad-jsonformat": "camel-case",
735
+ "tr-cache-control": "no-cache",
736
+ referer: `${APP_URL}/calendar/${usernameForReferer}`,
737
+ },
738
+ },
739
+ );
740
+ }
741
+
448
742
  async getPersonalRecordsByActivityIds(memberId, usernameForReferer, activityIds) {
449
743
  if (activityIds.length === 0) return {};
450
744
  const batches = chunk(activityIds, 100);