@0x3639/intervals-icu
    Preparing search index...

    Files: FIT, GPX, CSV and zip

    Every method that downloads a file resolves with a Node Buffer, ready to write to disk, pipe to a stream, or inspect directly:

    Method Format
    ActivityService.downloadFile Original uploaded activity file (gzip compressed)
    ActivityService.downloadFitFile Intervals.icu-generated FIT file for one activity
    ActivityService.downloadFitFiles Zip of Intervals.icu-generated FIT files for several activities
    ActivityService.downloadGPX GPX file, optionally with power/heart-rate extensions
    ActivityService.downloadActivitiesCSV All activities as CSV
    ActivityService.getStreamsCSV One activity's streams as CSV
    EventService.downloadWorkout One calendar workout in zwo, mrc, erg or fit format
    EventService.downloadWorkoutsZip Calendar workouts in a date range, zipped
    WorkoutService.convertWorkout A workout definition converted to zwo, mrc, erg or fit
    WorkoutService.convertWorkoutForAthlete Same, resolved against an athlete's own FTP/zones
    GearService.downloadCSV Gear list as CSV
    AnalyticsService.getCurvesCSV Activity power curves as CSV
    PerformanceService.getActivityPaceCurvesCSV Athlete-level pace curves as CSV
    const client = new IntervalsClient({ apiKey });

    const localDate = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
    const now = new Date(); // one timestamp for both bounds
    const yearAgo = new Date(now);
    yearAgo.setFullYear(yearAgo.getFullYear() - 1);
    const newest = localDate(now);
    const oldest = localDate(yearAgo);

    const activities = await client.activities.listActivities({ oldest, newest });
    const activity = activities.find((a) => a.type);

    if (!activity) {
    console.log('No activities with a type in the last year');
    } else {
    const id = activity.id!;

    const fit = await client.activities.downloadFitFile(id);
    // A fresh directory with a random name, rather than a predictable path in the shared
    // temp directory: nothing else can pre-create or read the file. `wx` refuses to write
    // to an existing path and 0o600 keeps the file readable only by the current user.
    const outDir = mkdtempSync(join(tmpdir(), 'intervals-icu-'));
    const fitPath = join(outDir, `${id}.fit`);
    writeFileSync(fitPath, fit, { mode: 0o600, flag: 'wx' });
    console.log(`FIT file: ${fit.length} bytes, written to ${fitPath} (left in place for inspection)`);

    const gpx = await client.activities.downloadGPX(id);
    console.log(`GPX file: ${gpx.length} bytes`);
    }

    const csv = await client.activities.downloadActivitiesCSV();
    console.log(`Activities CSV: ${csv.length} bytes`);

    Uploads take a Buffer | Blob | Uint8Array plus a filename, and are sent as multipart/form-data:

    CI never runs this example; running it by hand changes the authenticated account: it uploads a real activity and deletes it again in a finally block. If the upload response carries no activity id, the activity stays on the account and has to be deleted by hand. If the create call itself fails after the server committed it, nothing is cleaned up: search the account for Created by the SDK example (or Uploaded by the SDK example) and delete it by hand.

    // maxRetries: 0 — a create that committed before a 5xx would be repeated by a retry, and only the last response's id would be cleaned up.
    const client = new IntervalsClient({ apiKey, maxRetries: 0 });

    // A unique marker in the name: if the upload fails after the server committed it, this
    // is what to search the account for.
    const marker = `Uploaded by the SDK example ${Date.now()}`;

    const file = readFileSync(filePath);
    const uploaded = await client.activities.uploadActivity(file, basename(filePath), {
    name: marker,
    });

    // UploadResponse reports the created activities in `activities`, and a single-activity
    // upload also echoes an `id`. Collect whichever is present so the example can clean up
    // after itself.
    const created = (uploaded.activities ?? []).flatMap((a) => (typeof a.id === 'string' ? [a.id] : []));
    if (created.length === 0 && typeof uploaded.id === 'string') created.push(uploaded.id);

    try {
    console.log(`Uploaded, id=${created.join(', ') || 'not reported in the response'}`);
    } finally {
    for (const id of created) {
    try {
    await client.activities.deleteActivity(id);
    console.log(`Deleted activity ${id}`);
    } catch (err) {
    console.error(`Could not delete activity ${id}; remove it by hand:`, err);
    }
    }
    if (created.length === 0) {
    console.log('The upload response carried no activity id; the uploaded activity remains on the account');
    }
    }

    WorkoutService.convertWorkout and WorkoutService.convertWorkoutForAthlete take a WorkoutConversionInput body and a target WorkoutFormat. All four of name, description, type and workout_doc are required on that type, not just workout_doc: live probes recorded in AUDIT.md found that a body missing workout_doc gets HTTP 500 from the API, and the remaining fields were not probed individually, so the SDK treats all four as required as a conservative contract. A calendar workout event's own workout_doc field (from EventService.listEvents) is a convenient source for a real body, since a library or calendar workout already carries all four fields together. See API behaviour for the probe record.

    const client = new IntervalsClient({ apiKey });

    const localDate = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
    const now = new Date(); // one timestamp for both bounds
    const yearAgo = new Date(now);
    yearAgo.setFullYear(yearAgo.getFullYear() - 1);
    const newest = localDate(now);
    const oldest = localDate(yearAgo);

    // convertWorkout() requires name, description, type and workout_doc; check the runtime
    // shape of each rather than trusting the Event type, since API data is external.
    type ConvertibleEvent = Event & WorkoutConversionInput;
    const isConvertible = (e: Event): e is ConvertibleEvent =>
    typeof e.name === 'string' &&
    typeof e.description === 'string' &&
    typeof e.type === 'string' &&
    typeof e.workout_doc === 'object' && e.workout_doc !== null && !Array.isArray(e.workout_doc);

    const events = await client.events.listEvents({ oldest, newest, category: ['WORKOUT'] });
    const event = events.find(isConvertible);

    if (!event) {
    console.log('No calendar workouts found');
    } else {
    const zwo = await client.workouts.convertWorkout(
    { name: event.name, description: event.description, type: event.type, workout_doc: event.workout_doc },
    '.zwo',
    );
    console.log(zwo.toString('utf8').slice(0, 80));
    }

    const zip = await client.events.downloadWorkoutsZip({ ext: '.zwo', oldest, newest });
    console.log(`Workouts zip: ${zip.length} bytes`);

    Two related format details: