Skip to content

Instantly share code, notes, and snippets.

@jaimergp
Created January 22, 2024 12:28

Revisions

  1. jaimergp created this gist Jan 22, 2024.
    11 changes: 11 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,11 @@
    # From Zero To Hero to Hevy

    I had been using Zero To Hero for years to track my workouts. The app has not seen development in a while.

    You can backup your ZTH data to a Realm database via the app.
    This .zth file can be exported to a JSON with the attached Node.js script (you need `npm install realm`).

    Then the JSON can be post-processed into a CSV file following the Strong app format
    (I took this [sample](https://github.com/AlexandrosKyriakakis/StrongAppAnalytics/blob/main/Data/strong.csv?plain=1)).

    The resulting CSV can be imported via the Hevy Import/Export menu.
    139 changes: 139 additions & 0 deletions json_to_strong_csv.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,139 @@
    import csv
    import json
    from datetime import datetime, timedelta
    from functools import lru_cache


    def read_json(path):
    with open(path, "r") as f:
    return json.load(f)


    def write_csv(path, data):
    with open(path, "w") as f:
    writer = csv.writer(f)
    writer.writerows(data)


    def populate_csv_from_json(data):
    rows = [
    [
    "Date",
    "Workout Name",
    "Duration",
    "Exercise Name",
    "Set Order",
    "Weight",
    "Reps",
    "Distance",
    "Seconds",
    "Notes",
    "Workout Notes",
    "RPE",
    ]
    ]
    class Duration:
    def __init__(self, *timestamps):
    self.timestamps = list(timestamps)
    def append(self, timestamp):
    if timestamp:
    self.timestamps.append(timestamp)
    def __str__(self):
    dt = timedelta(milliseconds = max(self.timestamps) - min(self.timestamps))
    mm, ss = divmod(dt.total_seconds(), 60)
    hh, mm = divmod(mm, 60)
    if hh > 1:
    hh = 0
    if hh == 0 and mm == 0:
    return "1h 00m"
    return "%dh %02dm" % (hh, mm)

    for workout in data["Workout"]:
    timestamp = workout["date"] or 0
    date = datetime.fromtimestamp(timestamp / 1000).strftime("%Y-%m-%d %H:%M:%S")
    workout_name = workout["name"]
    duration = Duration(timestamp)
    for exercise in workout["exerciseList"]:
    for set_order, movement in enumerate(exercise["movementSetList"], 1):
    row = []
    duration.append(movement["date"])
    exercise_name = movement_map(movement["movement"])
    weight = movement["performedWeight"]
    reps = movement["reps"]
    row.extend(
    [
    date,
    workout_name,
    duration,
    exercise_name,
    set_order,
    weight,
    reps,
    "",
    "",
    "",
    "",
    "",
    ]
    )
    rows.append(row)

    for workout in data["CustomProgramData"]:
    ...
    # for exercise in data["Exercise"]:
    # ...
    return rows


    @lru_cache(maxsize=None)
    def movement_map(key):
    return {
    "1653663211431": "Butterfly reverse",
    "1693643373232": "Push-up",
    "1693645906260": "Pike push-ups",
    "BARBELLROW": "Barbell row",
    "BEHINDTHENECKPRESS": "Behind the neck press",
    "BENCHPRESS": "Bench press",
    "BENCHPRESS_DECLINE": "Declined bench press",
    "BENCHPRESS_INCLINE": "Inclined bench press",
    "BENCHPRESS_INCLINE_DUMBBELL": "Inclined bench press (dumbbell)",
    "BICEPS_CURL_HAMMER_DUMBBELL": "Biceps hammer curl (dumbbell)",
    "CALFRAISE": "Seated calf raise",
    "CALFRAISE_STANDING": "Standing calf raise",
    "CURL_BARBELL": "Barbell curl",
    "CURL_DUMBBELL": "Dumbbell curl",
    "DEADLIFT": "Deadlift",
    "DEADLIFT_ROMANIAN": "Romanian deadlift",
    "FLYS_DUMBBELL": "Dumbbell fly",
    "FLYS_INCLINE_CABLE": "Incline cable fly",
    "LEGPRESS": "Leg press",
    "LEG_CURL": "Leg curl",
    "LEG_EXTENSION": "Leg extension",
    "LUNGE_BACKSTEP": "Backstep lunge",
    "OVERHEADPRESS": "Overhead press",
    "PEC_FLY_MACHINE": "Pec fly machine",
    "PULLDOWN_LATERAL": "Lateral pull down",
    "PULLUP": "Pull-up",
    "PULL_FACE": "Face pull",
    "ROW_CABLE_SEATED": "Seated cable row",
    "ROW_DUMBBELL": "Row (dumbbell)",
    "ROW_PENDLAY": "Row (pendlay)",
    "SHOULDER_PRESS_SEATED_DUMBBELL": "Seated shoulder press (dumbbell)",
    "SHOULDER_RAISE_SIDE_LATERAL": "Lateral side raise",
    "SKULLCRUSHERS": "Skullcrushers",
    "SQUAT": "Squat",
    "SQUAT_FRONT": "Front squat",
    "TRICEPS_DIPS": "Dips",
    "TRICEP_EXTENSION_CABLE": "Cable tricep extension",
    "TRICEP_PUSHDOWNS": "Tricep pushdowns",
    }.get(key, key)


    def main():
    data = read_json("exported_data.json")
    rows = populate_csv_from_json(data)
    write_csv("exported_data.csv", rows)


    if __name__ == "__main__":
    main()
    36 changes: 36 additions & 0 deletions zth_to_json.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,36 @@
    const Realm = require('realm');
    const fs = require('fs');

    // Function to export entire Realm database to JSON
    async function exportRealmToJSON() {
    // Specify the file path for your Realm database
    const realmFile = './backup.zth';

    // Configure Realm with the specified file path
    const realm = await Realm.open({
    path: realmFile,
    readOnly: true,
    });

    // Get all objects from all available object types
    const allObjects = {};
    realm.schema.forEach((objectType) => {
    allObjects[objectType.name] = realm.objects(objectType.name);
    });

    // Convert to JSON
    const jsonData = JSON.stringify(allObjects, null, 2);

    // Print or save the JSON string as needed
    console.log(jsonData);

    // If you want to save to a file
    const exportFilePath = 'exported_data.json';
    fs.writeFileSync(exportFilePath, jsonData, 'utf-8');

    // Close the Realm instance
    realm.close();
    }

    // Call the function to export
    exportRealmToJSON();