Skip to content

Commit 8577c93

Browse files
committed
fitness track part 2
1 parent 8d2c145 commit 8577c93

File tree

20 files changed

+704
-10
lines changed

20 files changed

+704
-10
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import json
2+
from pprint import pprint
3+
4+
def seconds_to_human_friendly_time_string(seconds):
5+
hours = int(seconds / 3600)
6+
minutes = int((seconds % 3600) / 60)
7+
seconds = int(seconds % 60)
8+
# Handle zero cases
9+
if hours == 0 and minutes == 0 and seconds == 0:
10+
return "0 seconds"
11+
# Handle hours
12+
if hours > 0:
13+
return f"{hours} hours and {minutes} minutes and {seconds} seconds"
14+
# Handle minutes
15+
if minutes > 0:
16+
return f"{minutes} minutes and {seconds} seconds"
17+
# Handle seconds
18+
if seconds > 0:
19+
return f"{seconds} seconds"
20+
return "0 seconds"
21+
22+
with open("fitness_plan_.json", "r") as f:
23+
data = json.load(f)
24+
entries = data["exerciseLogEntries"]
25+
entries = [e for e in entries if e['userId'] == '89760760' and e["exerciseName"] != "Rest"]
26+
27+
"""
28+
Entry sample:
29+
{
30+
"tenantId": 1,
31+
"userId": "89760760",
32+
"exerciseId": 1555609163,
33+
"herculesExerciseId": "602621587d678600085608e2",
34+
"executionType": "TIMED",
35+
"meta": {
36+
"weightUnit": "KG",
37+
"durationUnit": "SECOND",
38+
"distanceUnit": "KILOMETRE"
39+
},
40+
"personalBest": {
41+
"id": 791910,
42+
"createdOn": "2023-02-12T08:38:49.000+00:00",
43+
"lastModifiedOn": "2023-02-12T08:38:49.000+00:00",
44+
"createdBy": "system",
45+
"version": 0,
46+
"tenantId": 1,
47+
"userId": "89760760",
48+
"herculesExerciseId": "602621587d678600085608e2",
49+
"userFitnessLevel": "INTERMEDIATE",
50+
"duration": 5400,
51+
"distance": 11.24,
52+
"fromTime": "2023-02-12T08:38:49Z"
53+
},
54+
"previousBest": {
55+
"duration": 1860,
56+
"distance": 4.04
57+
},
58+
"thumbnailUrl": "hercules/production/assets/movements/images/CROSS_TRAINER_v1631535828209_aa25d179-c9fa-4570-acc7-3d7a489b7ee9.jpg",
59+
"exerciseName": "Cross Trainer",
60+
"exerciseType": "DISTANCE",
61+
"lateral": "BILATERAL",
62+
"templateLog": {
63+
"sequence": null,
64+
"unilateralDirection": null,
65+
"weight": null,
66+
"duration": null,
67+
"count": null,
68+
"distance": null,
69+
"value1": "4.04",
70+
"unit1": "kms",
71+
"separator": "x",
72+
"value2": "31",
73+
"unit2": "mins"
74+
},
75+
"fpExecutionLogs": [
76+
{
77+
"sequence": null,
78+
"unilateralDirection": null,
79+
"weight": null,
80+
"duration": 1860,
81+
"count": null,
82+
"distance": 4.04,
83+
"value1": "4.04",
84+
"unit1": "kms",
85+
"separator": "/",
86+
"value2": "31",
87+
"unit2": "mins"
88+
}
89+
]
90+
}
91+
"""
92+
# Create a CSV showing off exercise name, type, lateral, personal_best in the format (count * weight kgs) or (distance (kms), duration (seconds)) whichever fields are non-null in personalBest json dict, time when this personal best was achieved, previous_best in the same format, time when this previous best was achieved, and the thumbnailUrl
93+
import csv
94+
with open("fitness_plan_.csv", "w", newline="") as csvfile:
95+
csv_writer = csv.writer(csvfile, delimiter="\t")
96+
csv_writer.writerow(["ExerciseName", "ExerciseType", "Lateral", "PersonalBest", "PersonalBestTime", "PreviousBest", "ThumbnailUrl"])
97+
for entry in entries:
98+
exerciseName = entry["exerciseName"]
99+
exerciseType = entry["exerciseType"]
100+
lateral = entry["lateral"]
101+
personalBest = entry.get("personalBest", {})
102+
personalBestStr = ""
103+
if len(personalBest) == 0:
104+
personalBestStr = "No personal best"
105+
if "duration" in personalBest:
106+
personalBestStr = f"{seconds_to_human_friendly_time_string(personalBest['duration'])}"
107+
if "distance" in personalBest:
108+
personalBestStr = f"{personalBest['distance']} kms in {personalBestStr}"
109+
elif "weight" in personalBest:
110+
personalBestStr = f"{personalBest['count']} * {personalBest['weight']} kgs"
111+
elif "count" in personalBest:
112+
personalBestStr = f"{personalBest['count']} reps"
113+
personalBestTime = ""
114+
if "fromTime" in personalBest:
115+
personalBestTime = personalBest["fromTime"]
116+
previousBest = entry.get("previousBest", {})
117+
previousBestStr = ""
118+
if "duration" in previousBest:
119+
previousBestStr = f"{seconds_to_human_friendly_time_string(previousBest['duration'])}"
120+
if "distance" in previousBest:
121+
previousBestStr = f"{previousBest['distance']} kms in {previousBestStr}"
122+
elif "weight" in previousBest:
123+
previousBestStr = f"{previousBest['count']} * {previousBest['weight']} kgs"
124+
thumbnailUrl = "https://cdn-images.cure.fit/www-curefit-com/image/upload/" + entry["thumbnailUrl"]
125+
csv_writer.writerow([exerciseName, exerciseType, lateral, personalBestStr, personalBestTime, previousBestStr, thumbnailUrl])
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import json
2+
import requests
3+
4+
# Replace <SECRET> with your actual secret token
5+
headers = {
6+
"accept": "application/json",
7+
# "cookie": "<SECRET>", # get from the cult website, any curl call, get the value of header `Cookie`...
8+
# "at": "<SECRET>", # For API call intercepted via http-toolkit, this is sent as a header in almost all API calls.. I guess one of cookie and at should be passed, I passed at here, and cookie in previous script..
9+
"osname": "android",
10+
"clientversion": "10.08",
11+
"connection": "Keep-Alive",
12+
"content-type": "application/json; charset=utf-8"
13+
}
14+
15+
wodID = ""
16+
url = f"https://www.cult.fit/api/v2/fitnessplanner/exercisesForLogging"
17+
response = requests.post(url, headers=headers, data=json.dumps({
18+
"exerciseIds": [1555609100, 1555609101, 1555609102, 1555609103, 1555609104, 1555609105, 1555609106, 1555609107, 1555609108, 1555609109, 1555609110, 1555609111, 1555609112, 1555609113, 1555609114, 1555609115, 1555609116, 1555609117, 1555609118, 1555609119, 1555609120, 1555609121, 1555609122, 1555609123, 1555609124, 1555609125, 1555609126, 1555609127, 1555609128, 1555609129, 1555609130, 1555609131, 1555609132, 1555609133, 1555609134, 1555609135, 1555609136, 1555609137, 1555609138, 1555609139, 1555609140, 1555609141, 1555609142, 1555609143, 1555609144, 1555609145, 1555609146, 1555609147, 1555609148, 1555609149, 1555609150, 1555609151, 1555609152, 1555609153, 1555609154, 1555609155, 1555609156, 1555609157, 1555609158, 1555609159, 1555609160, 1555609161, 1555609162, 1555609163, 1555609165, 1555609166, 1555609167, 1555609168, 1555609169, 1555609170, 1555609171, 1555609172, 1555609173, 1555609174, 1555609175, 1555609176, 1555609177, 1555609178, 1555609179, 1555609180, 1555609181, 1555609182, 1555609183, 1555609184, 1555609185, 1555609186, 1555609187, 1555609188, 1555609189, 1555609190, 1555609191, 1555609192, 1555609193, 1555609194, 1555609195, 1555609196, 1555609197, 1555609198, 1555609199, 1555609200, 1555609201, 1555609202, 1555609203, 1555609204, 1555609205, 1555609206, 1555609207, 1555609208, 1555609209, 1555609210, 1555609211, 1555609212, 1555609213, 1555609214, 1555609215, 1555609216, 1555609217, 1555609218, 1555609219, 1555609220, 1555609221, 1555609222, 1555609223, 1555609224, 1555609225, 1555609226, 1555609227, 1555609228, 1555609229, 1555609230, 1555609231, 1555609232, 1555609233, 1555609234, 1555609235, 1555609236, 1555609237, 1555609238, 1555609239, 1555609240, 1555609241, 1555609242, 1555609243, 1555609244, 1555609245, 1555609246, 1555609247, 1555609248, 1555609249, 1555609250, 1555609251, 1555609252, 1555609253, 1555609254, 1555609255, 1555609256, 1555609257, 1555609258, 1555609259, 1555609260, 1555609261, 1555609262, 1555609263, 1555609264, 1555609265, 1555609266, 1555609267, 1555609268, 1555609269, 1555609270, 1555609271, 1555609272, 1555609273, 1555609274, 1555609275, 1555609276, 1555609277, 1555609278, 1555609279, 1555609282, 1555609283, 1555609284, 1555609285, 1555609286, 1555609287, 1555609288, 1555609289, 1555609290, 1555609291, 1555609292, 1555609293, 1555609294, 1555609295, 1555609296, 1555609297, 1555609298, 1555609299]
19+
}))
20+
print(response.text)
21+
data = response.json()
22+
23+
from pprint import pprint
24+
pprint(data)
25+
26+
with open(f"fitness_plan_{wodID}.json", "w") as f:
27+
json.dump(data, f, indent=4)
Loading
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
+++
2+
title = 'Fitness Track Part 2'
3+
date = 2023-09-30T16:31:59+05:30
4+
draft = false
5+
+++
6+
7+
## Fitness Track Part 2
8+
9+
This post is continuation of [Fitness Track Part 1](/posts/fitness-track/).
10+
11+
I finally extracted out the existing personal best for exercises as well, and added a new page in the appsheet to display them.
12+
13+
This one was slight tricky to implement because there did not seem to be an API which fetches all exercise details, so on trial and error, when I selected an exercise to log it in the app, it seemed to be calling an API `/api/v2/fitnessplanner/exercisesForLogging` with corresponding exerciseId under `exerciseIds` list parameter. I generated ~2-300 IDs nearby that ID and passed in the API.
14+
15+
Earlier I was trying with ~500 IDs but the API kept crashing, and even with 300 IDs, some of the intermediate IDs required removal (God knows why), and finally I got a JSON which looked like this:
16+
17+
<details>
18+
<summary>JSON</summary>
19+
```json
20+
{
21+
"tenantId": 1,
22+
"userId": "89760760",
23+
"exerciseId": 1555609163,
24+
"herculesExerciseId": "602621587d678600085608e2",
25+
"executionType": "TIMED",
26+
"meta": {
27+
"weightUnit": "KG",
28+
"durationUnit": "SECOND",
29+
"distanceUnit": "KILOMETRE"
30+
},
31+
"personalBest": {
32+
"id": 791910,
33+
"createdOn": "2023-02-12T08:38:49.000+00:00",
34+
"lastModifiedOn": "2023-02-12T08:38:49.000+00:00",
35+
"createdBy": "system",
36+
"version": 0,
37+
"tenantId": 1,
38+
"userId": "89760760",
39+
"herculesExerciseId": "602621587d678600085608e2",
40+
"userFitnessLevel": "INTERMEDIATE",
41+
"duration": 5400,
42+
"distance": 11.24,
43+
"fromTime": "2023-02-12T08:38:49Z"
44+
},
45+
"previousBest": {
46+
"duration": 1860,
47+
"distance": 4.04
48+
},
49+
"thumbnailUrl": "hercules/production/assets/movements/images/CROSS_TRAINER_v1631535828209_aa25d179-c9fa-4570-acc7-3d7a489b7ee9.jpg",
50+
"exerciseName": "Cross Trainer",
51+
"exerciseType": "DISTANCE",
52+
"lateral": "BILATERAL",
53+
"templateLog": {
54+
"sequence": null,
55+
"unilateralDirection": null,
56+
"weight": null,
57+
"duration": null,
58+
"count": null,
59+
"distance": null,
60+
"value1": "4.04",
61+
"unit1": "kms",
62+
"separator": "x",
63+
"value2": "31",
64+
"unit2": "mins"
65+
},
66+
"fpExecutionLogs": [
67+
{
68+
"sequence": null,
69+
"unilateralDirection": null,
70+
"weight": null,
71+
"duration": 1860,
72+
"count": null,
73+
"distance": 4.04,
74+
"value1": "4.04",
75+
"unit1": "kms",
76+
"separator": "/",
77+
"value2": "31",
78+
"unit2": "mins"
79+
}
80+
]
81+
}
82+
```
83+
</details>
84+
85+
Most of the parameters are self explanatory, rest I found by analyzing some of the JSONs manually. The analysis script using which I finally created a CSV is [analysis.py](./analysis.py), and the script to extract those JSONs is [cult_data_exercises.py](./cult_data_exercises.py).
86+
87+
Finally, it looks in my app like this:
88+
89+
![personal bests](images/image.png)

content/posts/fitness-track/cult_data_extract.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
# Replace <SECRET> with your actual secret token
88
headers = {
99
"accept": "application/json",
10-
"cookie": "", # get from the cult website, any curl call, get the value of header `Cookie`...
10+
"cookie": "<SECRET>", # get from the cult website, any curl call, get the value of header `Cookie`...
1111
"clientversion": "10.08",
1212
"connection": "Keep-Alive",
1313
"content-type": "application/json; charset=utf-8"

public/categories/index.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ <h2 class="side-title">
7878

7979
<ul>
8080

81+
<li>
82+
<a href="/posts/fitness-track-part-2/">Fitness Track Part 2</a>
83+
</li>
84+
8185
<li>
8286
<a href="/posts/fitness-track/">Fitness Track</a>
8387
</li>

public/index.html

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,28 @@
7171

7272

7373

74+
<section class="list-item">
75+
<h1 class="title"><a href="/posts/fitness-track-part-2/">Fitness Track Part 2</a></h1>
76+
77+
<div class="tips">
78+
<div class="date">
79+
<time datetime="2023-09-30 16:31:59 &#43;0530 IST">2023/09/30</time>
80+
</div>
81+
82+
83+
84+
85+
</div>
86+
87+
<div class="summary">
88+
89+
Fitness Track Part 2 🔗This post is continuation of Fitness Track Part 1.
90+
I finally extracted out the existing personal best for exercises as well, and added a new page in the appsheet to display them.
91+
This one was slight tricky to implement because there did not seem to be an API which fetches all exercise details, so on trial and error, when I selected an exercise to log it in the app, it seemed to be calling an API /api/v2/fitnessplanner/exercisesForLogging with corresponding exerciseId under exerciseIds list parameter.
92+
93+
</div>
94+
</section>
95+
7496
<section class="list-item">
7597
<h1 class="title"><a href="/posts/fitness-track/">Fitness Track</a></h1>
7698

@@ -158,6 +180,10 @@ <h2 class="side-title">
158180

159181
<ul>
160182

183+
<li>
184+
<a href="/posts/fitness-track-part-2/">Fitness Track Part 2</a>
185+
</li>
186+
161187
<li>
162188
<a href="/posts/fitness-track/">Fitness Track</a>
163189
</li>

public/index.xml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,24 @@
66
<description>Recent content on My Personal Blog</description>
77
<generator>Hugo -- gohugo.io</generator>
88
<language>en-us</language>
9-
<lastBuildDate>Fri, 29 Sep 2023 20:48:38 +0530</lastBuildDate><atom:link href="https://singhcoder.github.io/index.xml" rel="self" type="application/rss+xml" />
9+
<lastBuildDate>Sat, 30 Sep 2023 16:31:59 +0530</lastBuildDate><atom:link href="https://singhcoder.github.io/index.xml" rel="self" type="application/rss+xml" />
10+
<item>
11+
<title>Fitness Track Part 2</title>
12+
<link>https://singhcoder.github.io/posts/fitness-track-part-2/</link>
13+
<pubDate>Sat, 30 Sep 2023 16:31:59 +0530</pubDate>
14+
15+
<guid>https://singhcoder.github.io/posts/fitness-track-part-2/</guid>
16+
<description>&lt;h2 id=&#34;fitness-track-part-2&#34;&gt;Fitness Track Part 2 &lt;a href=&#34;#fitness-track-part-2&#34; class=&#34;anchor&#34;&gt;🔗&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;This post is continuation of &lt;a href=&#34;https://singhcoder.github.io/posts/fitness-track/&#34;&gt;Fitness Track Part 1&lt;/a&gt;.&lt;/p&gt;
17+
&lt;p&gt;I finally extracted out the existing personal best for exercises as well, and added a new page in the appsheet to display them.&lt;/p&gt;
18+
&lt;p&gt;This one was slight tricky to implement because there did not seem to be an API which fetches all exercise details, so on trial and error, when I selected an exercise to log it in the app, it seemed to be calling an API &lt;code&gt;/api/v2/fitnessplanner/exercisesForLogging&lt;/code&gt; with corresponding exerciseId under &lt;code&gt;exerciseIds&lt;/code&gt; list parameter. I generated ~2-300 IDs nearby that ID and passed in the API.&lt;/p&gt;
19+
&lt;p&gt;Earlier I was trying with ~500 IDs but the API kept crashing, and even with 300 IDs, some of the intermediate IDs required removal (God knows why), and finally I got a JSON which looked like this:&lt;/p&gt;
20+
&lt;!-- raw HTML omitted --&gt;
21+
&lt;p&gt;Most of the parameters are self explanatory, rest I found by analyzing some of the JSONs manually. The analysis script using which I finally created a CSV is &lt;a href=&#34;./analysis.py&#34;&gt;analysis.py&lt;/a&gt;, and the script to extract those JSONs is &lt;a href=&#34;./cult_data_exercises.py&#34;&gt;cult_data_exercises.py&lt;/a&gt;.&lt;/p&gt;
22+
&lt;p&gt;Finally, it looks in my app like this:&lt;/p&gt;
23+
&lt;p&gt;&lt;img src=&#34;images/image.png&#34; alt=&#34;personal bests&#34;&gt;&lt;/p&gt;
24+
</description>
25+
</item>
26+
1027
<item>
1128
<title>Fitness Track</title>
1229
<link>https://singhcoder.github.io/posts/fitness-track/</link>

0 commit comments

Comments
 (0)