mirror of
https://github.com/zoffline/zwift-offline.git
synced 2025-12-12 07:40:36 -08:00
Add fitness service goals
This commit is contained in:
@@ -1447,6 +1447,7 @@
|
||||
},
|
||||
{
|
||||
"name": "game_1_86_fitness-service_goals",
|
||||
"value": true,
|
||||
"values": {
|
||||
"fields": {
|
||||
"enable_KJ_cal_SP": {
|
||||
|
||||
@@ -15,6 +15,7 @@ all:
|
||||
protoc --python_out=. playback.proto
|
||||
protoc --python_out=. route-result.proto
|
||||
protoc --python_out=. user_storage.proto
|
||||
protoc --python_out=. fitness.proto
|
||||
|
||||
clean:
|
||||
rm -f *_pb2.py *_pb2.pyc
|
||||
|
||||
@@ -90,6 +90,12 @@ message ClubAttribution {
|
||||
optional float value = 2;
|
||||
}
|
||||
|
||||
message PacePartnerData {
|
||||
optional float wkg = 1;
|
||||
optional float time = 2;
|
||||
optional uint64 player_id = 3;
|
||||
}
|
||||
|
||||
enum ProfileFollowStatus {
|
||||
PFS_UNKNOWN = 1;
|
||||
PFS_REQUESTS_TO_FOLLOW = 2;
|
||||
@@ -147,6 +153,14 @@ message Activity { //where is primaryImageUrl, feedImageThumbnailUrl, activityR
|
||||
optional string club_name = 39;
|
||||
optional int64 movingTimeInMs = 40;
|
||||
//repeated ClubAttribution cas = 41;
|
||||
//optional PacePartnerData pp_data = 42;
|
||||
//optional int64 f43 = 43; // always 0
|
||||
optional float work = 44;
|
||||
optional float tss = 45;
|
||||
optional float normalized_power = 46;
|
||||
//optional int32 f47 = 47; // always -120
|
||||
repeated uint32 power_zones = 48;
|
||||
optional float power_units = 49; // factory tour challenge
|
||||
}
|
||||
|
||||
message ActivityList {
|
||||
|
||||
File diff suppressed because one or more lines are too long
62
protobuf/fitness.proto
Normal file
62
protobuf/fitness.proto
Normal file
@@ -0,0 +1,62 @@
|
||||
syntax = "proto2";
|
||||
|
||||
message Fitness {
|
||||
optional uint32 streak = 1;
|
||||
optional WeekMetrics this_week = 2;
|
||||
optional WeekMetrics last_week = 3;
|
||||
optional SportGoals goals = 4;
|
||||
optional uint32 f5 = 5;
|
||||
}
|
||||
|
||||
message WeekMetrics {
|
||||
optional string start = 1;
|
||||
optional float fitness_score = 2;
|
||||
optional uint32 distance = 3;
|
||||
optional uint32 elevation = 4;
|
||||
optional uint32 moving_time = 5;
|
||||
optional uint32 work = 6;
|
||||
optional uint32 calories = 7;
|
||||
optional float tss = 8;
|
||||
repeated DayMetrics days = 9;
|
||||
optional string status = 10;
|
||||
}
|
||||
|
||||
message DayMetrics {
|
||||
optional string day = 1;
|
||||
optional uint32 distance = 2;
|
||||
optional uint32 elevation = 3;
|
||||
optional uint32 moving_time = 4;
|
||||
optional uint32 work = 5;
|
||||
optional uint32 calories = 6;
|
||||
optional float tss = 7;
|
||||
repeated PowerZonePercentages power_zones = 8;
|
||||
}
|
||||
|
||||
message PowerZonePercentages {
|
||||
optional uint32 zone = 1;
|
||||
optional float percentage = 2;
|
||||
}
|
||||
|
||||
message SportGoals {
|
||||
optional GoalMetrics all = 1;
|
||||
optional GoalMetrics cycling = 2;
|
||||
optional GoalMetrics running = 3;
|
||||
optional GoalSetting current_goal = 4;
|
||||
optional uint64 last_updated = 5;
|
||||
}
|
||||
|
||||
message GoalMetrics {
|
||||
optional uint32 tss = 1;
|
||||
optional uint32 calories = 2;
|
||||
optional uint32 work = 3;
|
||||
optional uint32 distance = 4;
|
||||
optional uint32 moving_time = 5;
|
||||
}
|
||||
|
||||
enum GoalSetting { // can't conflict with GoalType from goal.proto
|
||||
TSS_GOAL = 0;
|
||||
KJ_GOAL = 1;
|
||||
CALORIES_GOAL = 2;
|
||||
DISTANCE_GOAL = 3;
|
||||
TIME_GOAL = 4;
|
||||
}
|
||||
37
protobuf/fitness_pb2.py
Normal file
37
protobuf/fitness_pb2.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: fitness.proto
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf.internal import builder as _builder
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rfitness.proto\"\x83\x01\n\x07\x46itness\x12\x0e\n\x06streak\x18\x01 \x01(\r\x12\x1f\n\tthis_week\x18\x02 \x01(\x0b\x32\x0c.WeekMetrics\x12\x1f\n\tlast_week\x18\x03 \x01(\x0b\x32\x0c.WeekMetrics\x12\x1a\n\x05goals\x18\x04 \x01(\x0b\x32\x0b.SportGoals\x12\n\n\x02\x66\x35\x18\x05 \x01(\r\"\xc5\x01\n\x0bWeekMetrics\x12\r\n\x05start\x18\x01 \x01(\t\x12\x15\n\rfitness_score\x18\x02 \x01(\x02\x12\x10\n\x08\x64istance\x18\x03 \x01(\r\x12\x11\n\televation\x18\x04 \x01(\r\x12\x13\n\x0bmoving_time\x18\x05 \x01(\r\x12\x0c\n\x04work\x18\x06 \x01(\r\x12\x10\n\x08\x63\x61lories\x18\x07 \x01(\r\x12\x0b\n\x03tss\x18\x08 \x01(\x02\x12\x19\n\x04\x64\x61ys\x18\t \x03(\x0b\x32\x0b.DayMetrics\x12\x0e\n\x06status\x18\n \x01(\t\"\xac\x01\n\nDayMetrics\x12\x0b\n\x03\x64\x61y\x18\x01 \x01(\t\x12\x10\n\x08\x64istance\x18\x02 \x01(\r\x12\x11\n\televation\x18\x03 \x01(\r\x12\x13\n\x0bmoving_time\x18\x04 \x01(\r\x12\x0c\n\x04work\x18\x05 \x01(\r\x12\x10\n\x08\x63\x61lories\x18\x06 \x01(\r\x12\x0b\n\x03tss\x18\x07 \x01(\x02\x12*\n\x0bpower_zones\x18\x08 \x03(\x0b\x32\x15.PowerZonePercentages\"8\n\x14PowerZonePercentages\x12\x0c\n\x04zone\x18\x01 \x01(\r\x12\x12\n\npercentage\x18\x02 \x01(\x02\"\x9f\x01\n\nSportGoals\x12\x19\n\x03\x61ll\x18\x01 \x01(\x0b\x32\x0c.GoalMetrics\x12\x1d\n\x07\x63ycling\x18\x02 \x01(\x0b\x32\x0c.GoalMetrics\x12\x1d\n\x07running\x18\x03 \x01(\x0b\x32\x0c.GoalMetrics\x12\"\n\x0c\x63urrent_goal\x18\x04 \x01(\x0e\x32\x0c.GoalSetting\x12\x14\n\x0clast_updated\x18\x05 \x01(\x04\"a\n\x0bGoalMetrics\x12\x0b\n\x03tss\x18\x01 \x01(\r\x12\x10\n\x08\x63\x61lories\x18\x02 \x01(\r\x12\x0c\n\x04work\x18\x03 \x01(\r\x12\x10\n\x08\x64istance\x18\x04 \x01(\r\x12\x13\n\x0bmoving_time\x18\x05 \x01(\r*]\n\x0bGoalSetting\x12\x0c\n\x08TSS_GOAL\x10\x00\x12\x0b\n\x07KJ_GOAL\x10\x01\x12\x11\n\rCALORIES_GOAL\x10\x02\x12\x11\n\rDISTANCE_GOAL\x10\x03\x12\r\n\tTIME_GOAL\x10\x04')
|
||||
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'fitness_pb2', globals())
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
_GOALSETTING._serialized_start=845
|
||||
_GOALSETTING._serialized_end=938
|
||||
_FITNESS._serialized_start=18
|
||||
_FITNESS._serialized_end=149
|
||||
_WEEKMETRICS._serialized_start=152
|
||||
_WEEKMETRICS._serialized_end=349
|
||||
_DAYMETRICS._serialized_start=352
|
||||
_DAYMETRICS._serialized_end=524
|
||||
_POWERZONEPERCENTAGES._serialized_start=526
|
||||
_POWERZONEPERCENTAGES._serialized_end=582
|
||||
_SPORTGOALS._serialized_start=585
|
||||
_SPORTGOALS._serialized_end=744
|
||||
_GOALMETRICS._serialized_start=746
|
||||
_GOALMETRICS._serialized_end=843
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
@@ -15,5 +15,6 @@ protoc --python_out=. variants.proto
|
||||
protoc --python_out=. playback.proto
|
||||
protoc --python_out=. route-result.proto
|
||||
protoc --python_out=. user_storage.proto
|
||||
protoc --python_out=. fitness.proto
|
||||
|
||||
pause
|
||||
@@ -159,6 +159,15 @@ message PlayerState {
|
||||
optional uint32 ps_f37 = 37; // = f(BikeEntity.field_2a28) BikeEntity::CreateNewPacket
|
||||
optional bool canSteer = 38; // = BikeEntity.m_canSteer
|
||||
optional int32 route = 39;
|
||||
optional int32 pacerBotGroupSize = 40;
|
||||
optional bool activeSteer = 41;
|
||||
optional bool portal = 43;
|
||||
optional int32 portalGradientScale = 44; // 0 = 50%, 1 = 75%, 2 = 100%, 3 = 125%
|
||||
optional int32 portalElevationScale = 45; // 50, 75, 100 or 125
|
||||
optional int32 boostPad = 46;
|
||||
optional int32 hazardPad = 47;
|
||||
optional int32 timeBonus = 48;
|
||||
optional int32 rideonBomb = 49; // always seems to be x5 with value of 9
|
||||
}
|
||||
|
||||
message ClientToServer {
|
||||
|
||||
File diff suppressed because one or more lines are too long
165
zwift_offline.py
165
zwift_offline.py
@@ -58,6 +58,7 @@ import variants_pb2
|
||||
import playback_pb2
|
||||
import user_storage_pb2
|
||||
import online_sync
|
||||
import fitness_pb2
|
||||
|
||||
logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
|
||||
logger = logging.getLogger('zoffline')
|
||||
@@ -253,6 +254,11 @@ class Activity(db.Model):
|
||||
fitness_privacy = db.Column(db.Integer)
|
||||
club_name = db.Column(db.Text)
|
||||
movingTimeInMs = db.Column(db.Integer)
|
||||
work = db.Column(db.Float)
|
||||
tss = db.Column(db.Float)
|
||||
normalized_power = db.Column(db.Float)
|
||||
power_zones = db.Column(db.Text)
|
||||
power_units = db.Column(db.Float)
|
||||
|
||||
class SegmentResult(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@@ -323,6 +329,18 @@ class Goal(db.Model):
|
||||
status = db.Column(db.Integer)
|
||||
timezone = db.Column(db.Text)
|
||||
|
||||
class GoalMetrics(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
player_id = db.Column(db.Integer)
|
||||
weekGoalTSS = db.Column(db.Integer)
|
||||
weekGoalCalories = db.Column(db.Integer)
|
||||
weekGoalKjs = db.Column(db.Integer)
|
||||
weekGoalDistanceKilometers = db.Column(db.Float)
|
||||
weekGoalDistanceMiles = db.Column(db.Float)
|
||||
weekGoalTimeMinutes = db.Column(db.Integer)
|
||||
lastUpdated = db.Column(db.Text)
|
||||
currentGoalSetting = db.Column(db.Text)
|
||||
|
||||
class Playback(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
player_id = db.Column(db.Integer, nullable=False)
|
||||
@@ -391,6 +409,7 @@ class PartialProfile:
|
||||
male = True
|
||||
weight_in_grams = 0
|
||||
imageSrc = ''
|
||||
use_metric = True
|
||||
time = 0
|
||||
def to_json(self):
|
||||
return {"countryCode": self.country_code,
|
||||
@@ -514,6 +533,7 @@ def get_partial_profile(player_id):
|
||||
partial_profile.player_type = profile_pb2.PlayerType.Name(jsf(profile, 'player_type', 1))
|
||||
partial_profile.male = profile.is_male
|
||||
partial_profile.weight_in_grams = profile.weight_in_grams
|
||||
partial_profile.use_metric = profile.use_metric
|
||||
player_partial_profiles[player_id] = partial_profile
|
||||
player_partial_profiles[player_id].time = time.monotonic()
|
||||
return player_partial_profiles[player_id]
|
||||
@@ -1111,11 +1131,14 @@ def logout(username):
|
||||
return redirect(url_for('login'))
|
||||
|
||||
|
||||
def insert_protobuf_into_db(table_name, msg, exclude_fields=[]):
|
||||
def insert_protobuf_into_db(table_name, msg, exclude_fields=[], json_fields=[]):
|
||||
msg_dict = MessageToDict(msg, preserving_proto_field_name=True, use_integers_for_enums=True)
|
||||
for key in exclude_fields:
|
||||
if key in msg_dict:
|
||||
del msg_dict[key]
|
||||
for key in json_fields:
|
||||
if key in msg_dict:
|
||||
msg_dict[key] = json.dumps(msg_dict[key])
|
||||
if 'id' in msg_dict:
|
||||
del msg_dict['id']
|
||||
row = table_name(**msg_dict)
|
||||
@@ -1124,11 +1147,14 @@ def insert_protobuf_into_db(table_name, msg, exclude_fields=[]):
|
||||
return row.id
|
||||
|
||||
|
||||
def update_protobuf_in_db(table_name, msg, id, exclude_fields=[]):
|
||||
def update_protobuf_in_db(table_name, msg, id, exclude_fields=[], json_fields=[]):
|
||||
msg_dict = MessageToDict(msg, preserving_proto_field_name=True, use_integers_for_enums=True)
|
||||
for key in exclude_fields:
|
||||
if key in msg_dict:
|
||||
del msg_dict[key]
|
||||
for key in json_fields:
|
||||
if key in msg_dict:
|
||||
msg_dict[key] = json.dumps(msg_dict[key])
|
||||
table_name.query.filter_by(id=id).update(msg_dict)
|
||||
db.session.commit()
|
||||
|
||||
@@ -1214,6 +1240,7 @@ def select_activities_json(player_id, limit, start_after=None):
|
||||
return ret
|
||||
|
||||
@app.route('/api/activity-feed/feed/', methods=['GET'])
|
||||
@app.route('/api/activity-feed-service-v2/feed/just-me', methods=['GET'])
|
||||
@jwt_to_session_cookie
|
||||
@login_required
|
||||
def api_activity_feed():
|
||||
@@ -1815,7 +1842,7 @@ def do_api_profiles(profile_id, is_json):
|
||||
@jwt_to_session_cookie
|
||||
@login_required
|
||||
def api_profiles_me():
|
||||
if request.headers['Source'] == "zwift-companion":
|
||||
if request.headers['Accept'] == 'application/json':
|
||||
return do_api_profiles(current_user.player_id, True)
|
||||
else:
|
||||
return do_api_profiles(current_user.player_id, False)
|
||||
@@ -2036,7 +2063,7 @@ def api_profiles_activities(player_id):
|
||||
return '', 401
|
||||
activity = activity_pb2.Activity()
|
||||
activity.ParseFromString(request.stream.read())
|
||||
activity.id = insert_protobuf_into_db(Activity, activity, ['fit'])
|
||||
activity.id = insert_protobuf_into_db(Activity, activity, ['fit'], ['power_zones'])
|
||||
return '{"id": %ld}' % activity.id, 200
|
||||
|
||||
# request.method == 'GET'
|
||||
@@ -2044,7 +2071,7 @@ def api_profiles_activities(player_id):
|
||||
rows = db.session.execute(sqlalchemy.text("SELECT * FROM activity WHERE player_id = :p AND date > date('now', '-1 month')"), {"p": player_id}).mappings()
|
||||
for row in rows:
|
||||
activity = activities.activities.add()
|
||||
row_to_protobuf(row, activity, exclude_fields=['fit'])
|
||||
row_to_protobuf(row, activity, exclude_fields=['fit', 'power_zones'])
|
||||
return activities.SerializeToString(), 200
|
||||
|
||||
@app.route('/api/profiles/<int:player_id>/activities/<int:activity_id>/images', methods=['POST'])
|
||||
@@ -2118,7 +2145,10 @@ def api_profiles():
|
||||
p.CopyFrom(random_profile(profile))
|
||||
p.id = p_id
|
||||
p.first_name = ''
|
||||
p.last_name = time_since(global_ghosts[player_id].play[ghostId-1].date)
|
||||
try: # profile can be requested after ghost is deleted
|
||||
p.last_name = time_since(global_ghosts[player_id].play[ghostId-1].date)
|
||||
except:
|
||||
p.last_name = 'Ghost'
|
||||
p.country_code = 0
|
||||
if GHOST_PROFILE:
|
||||
for item in ['country_code', 'ride_jersey', 'bike_frame', 'bike_frame_colour', 'bike_wheel_front', 'bike_wheel_rear', 'ride_helmet_type', 'glasses_type', 'ride_shoes_type', 'ride_socks_type']:
|
||||
@@ -2404,12 +2434,18 @@ def api_profiles_activities_id(player_id, activity_id):
|
||||
stream = request.stream.read()
|
||||
activity = activity_pb2.Activity()
|
||||
activity.ParseFromString(stream)
|
||||
update_protobuf_in_db(Activity, activity, activity_id, ['fit'])
|
||||
update_protobuf_in_db(Activity, activity, activity_id, ['fit'], ['power_zones'])
|
||||
|
||||
response = '{"id":%s}' % activity_id
|
||||
if request.args.get('upload-to-strava') != 'true':
|
||||
return response, 200
|
||||
|
||||
if activity.distanceInMeters == 0: # Zwift saves the current activity when joining events, even if the distance is zero
|
||||
Activity.query.filter_by(id=activity_id).delete()
|
||||
db.session.commit()
|
||||
logout_player(player_id)
|
||||
return response, 200
|
||||
|
||||
create_power_curve(player_id, BytesIO(activity.fit))
|
||||
save_fit(player_id, '%s - %s' % (activity_id, activity.fit_filename), activity.fit)
|
||||
if current_user.enable_ghosts:
|
||||
@@ -3782,6 +3818,120 @@ def update_streaks(player_id, activity):
|
||||
def api_fitness_streaks():
|
||||
return get_streaks(current_user.player_id).SerializeToString(), 200
|
||||
|
||||
@app.route('/api/fitness/metrics-and-goals', methods=['GET']) # TODO: fitnessScore, trainingStatus, numStreakSavers, givenXp, better default goals
|
||||
@jwt_to_session_cookie
|
||||
@login_required
|
||||
def api_fitness_metrics_and_goals():
|
||||
if request.headers['Accept'] == 'application/json':
|
||||
try:
|
||||
date = datetime.datetime.strptime(request.args.get('month') + request.args.get('weekOf') + request.args.get('year'), "%m%d%Y")
|
||||
except:
|
||||
return '', 404
|
||||
fitness = {"fitnessMetrics": []}
|
||||
for i in range(2):
|
||||
start, end = get_week_range(date - datetime.timedelta(days=i * 7))
|
||||
stmt = sqlalchemy.text("""SELECT SUM(distanceInMeters), SUM(total_elevation), SUM(movingTimeInMs), SUM(work), SUM(calories), SUM(tss)
|
||||
FROM activity WHERE player_id = :p AND strftime('%s', start_date) >= strftime('%s', :s) AND strftime('%s', start_date) <= strftime('%s', :e)""")
|
||||
row = db.session.execute(stmt, {"p": current_user.player_id, "s": start, "e": end}).first()
|
||||
week = {"startOfWeek": start.strftime('%Y-%m-%d'), "fitnessScore": 0, "totalDistanceKilometers": row[0] / 1000 if row[0] else 0,
|
||||
"totalElevationMeters": int(row[1]) if row[1] else 0, "totalDurationMinutes": int(row[2] / 60000) if row[2] else 0,
|
||||
"totalKilojoules": int(row[3]) if row[3] else 0, "totalCalories": int(row[4]) if row[4] else 0,
|
||||
"totalTSS": row[5] if row[5] else 0, "useMetric": get_partial_profile(current_user.player_id).use_metric,
|
||||
"weekStreak": get_streaks(current_user.player_id).cur_streak, "numStreakSavers": 0, "days": {}, "trainingStatus": "FRESH"}
|
||||
for i in range(0, 7):
|
||||
day = start + datetime.timedelta(days=i)
|
||||
stmt = sqlalchemy.text("""SELECT SUM(distanceInMeters), SUM(total_elevation), SUM(movingTimeInMs), SUM(work), SUM(calories), SUM(tss)
|
||||
FROM activity WHERE player_id = :p AND strftime('%F', start_date) = strftime('%F', :d)""")
|
||||
row = db.session.execute(stmt, {"p": current_user.player_id, "d": day}).first()
|
||||
if row[0]:
|
||||
d = {"day": day.strftime('%a').lower(), "distanceKilometers": row[0] / 1000, "elevationMeters": int(row[1]) if row[1] else 0,
|
||||
"durationMinutes": int(row[2] / 60000) if row[2] else 0, "kilojoules": int(row[3]) if row[3] else 0,
|
||||
"calories": int(row[4]) if row[4] else 0, "tss": row[5] if row[5] else 0,
|
||||
"powerZonePercentages": {"1": 1, "2": 0, "3": 0, "4": 0, "5": 0, "6": 0, "7": 0}, "givenXp": 0}
|
||||
zones = [0] * 7
|
||||
stmt = sqlalchemy.text("SELECT power_zones FROM activity WHERE player_id = :p AND strftime('%F', start_date) = strftime('%F', :d)")
|
||||
for row in db.session.execute(stmt, {"p": current_user.player_id, "d": day}):
|
||||
if row.power_zones:
|
||||
zones = [a + b for a, b in zip(zones, json.loads(row.power_zones))]
|
||||
total = sum(zones)
|
||||
if total:
|
||||
for i in range(0, 7):
|
||||
d["powerZonePercentages"][str(i + 1)] = zones[i] / total
|
||||
week["days"][d["day"]] = d
|
||||
fitness["fitnessMetrics"].append(week)
|
||||
end = get_week_range(date)[1].strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + 'Z'
|
||||
row = GoalMetrics.query.filter(GoalMetrics.player_id == current_user.player_id, GoalMetrics.lastUpdated <= end).order_by(GoalMetrics.lastUpdated.desc()).first()
|
||||
cycling = {"weekGoalTSS": row.weekGoalTSS if row else 200, "weekGoalCalories": row.weekGoalCalories if row else 2000,
|
||||
"weekGoalKjs": row.weekGoalKjs if row else 2000, "weekGoalDistanceKilometers": row.weekGoalDistanceKilometers if row else 100,
|
||||
"weekGoalTimeMinutes": row.weekGoalTimeMinutes if row else 180,
|
||||
"lastUpdated": row.lastUpdated if row else datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + 'Z'}
|
||||
fitness["goalsMetrics"] = {"all": cycling, "cycling": cycling, "running": None, "currentGoalSetting": row.currentGoalSetting if row else "DISTANCE"}
|
||||
return jsonify(fitness)
|
||||
else:
|
||||
fitness = fitness_pb2.Fitness()
|
||||
fitness.streak = get_streaks(current_user.player_id).cur_streak
|
||||
for i, week in enumerate([fitness.this_week, fitness.last_week]):
|
||||
start, end = get_week_range(datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=i * 7))
|
||||
week.start = start.strftime('%Y-%m-%d')
|
||||
stmt = sqlalchemy.text("""SELECT SUM(distanceInMeters), SUM(total_elevation), SUM(movingTimeInMs), SUM(work), SUM(calories), SUM(tss)
|
||||
FROM activity WHERE player_id = :p AND strftime('%s', start_date) >= strftime('%s', :s) AND strftime('%s', start_date) <= strftime('%s', :e)""")
|
||||
row = db.session.execute(stmt, {"p": current_user.player_id, "s": start, "e": end}).first()
|
||||
week.fitness_score = 0
|
||||
week.distance = int(row[0]) if row[0] else 0
|
||||
week.elevation = int(row[1]) if row[1] else 0
|
||||
week.moving_time = int(round(row[2], -4)) if row[2] else 0
|
||||
week.work = int(row[3]) if row[3] else 0
|
||||
week.calories = int(row[4]) if row[4] else 0
|
||||
week.tss = row[5] if row[5] else 0
|
||||
week.status = "FRESH"
|
||||
for i in range(0, 7):
|
||||
day = start + datetime.timedelta(days=i)
|
||||
stmt = sqlalchemy.text("""SELECT SUM(distanceInMeters), SUM(total_elevation), SUM(movingTimeInMs), SUM(work), SUM(calories), SUM(tss)
|
||||
FROM activity WHERE player_id = :p AND strftime('%F', start_date) = strftime('%F', :d)""")
|
||||
row = db.session.execute(stmt, {"p": current_user.player_id, "d": day}).first()
|
||||
if row[0]:
|
||||
d = week.days.add()
|
||||
d.day = day.strftime('%a').lower()
|
||||
d.distance = int(row[0])
|
||||
d.elevation = int(row[1]) if row[1] else 0
|
||||
d.moving_time = int(round(row[2], -4)) if row[2] else 0
|
||||
d.work = int(row[3]) if row[3] else 0
|
||||
d.calories = int(row[4]) if row[4] else 0
|
||||
d.tss = row[5] if row[5] else 0
|
||||
zones = [0] * 7
|
||||
stmt = sqlalchemy.text("SELECT power_zones FROM activity WHERE player_id = :p AND strftime('%F', start_date) = strftime('%F', :d)")
|
||||
for row in db.session.execute(stmt, {"p": current_user.player_id, "d": day}):
|
||||
if row.power_zones:
|
||||
zones = [a + b for a, b in zip(zones, json.loads(row.power_zones))]
|
||||
total = sum(zones)
|
||||
if total:
|
||||
for i in range(0, 7):
|
||||
pz = d.power_zones.add()
|
||||
pz.zone = i + 1
|
||||
pz.percentage = zones[i] / total
|
||||
row = GoalMetrics.query.filter_by(player_id=current_user.player_id).order_by(GoalMetrics.lastUpdated.desc()).first()
|
||||
for sport in [fitness.goals.all, fitness.goals.cycling]:
|
||||
sport.tss = row.weekGoalTSS if row else 200
|
||||
sport.calories = row.weekGoalCalories if row else 2000
|
||||
sport.work = row.weekGoalKjs if row else 2000
|
||||
sport.distance = (int(row.weekGoalDistanceKilometers) if row else 100) * 1000
|
||||
sport.moving_time = (row.weekGoalTimeMinutes if row else 180) * 60000
|
||||
fitness.goals.current_goal = fitness_pb2.GoalSetting.Value(row.currentGoalSetting + "_GOAL" if row else "DISTANCE_GOAL")
|
||||
last_updated = datetime.datetime.strptime(row.lastUpdated, "%Y-%m-%dT%H:%M:%S.%f%z") if row else datetime.datetime.now(datetime.timezone.utc)
|
||||
fitness.goals.last_updated = int(last_updated.timestamp() * 1000)
|
||||
return fitness.SerializeToString(), 200
|
||||
|
||||
@app.route('/api/fitness/fitness-goals/history', methods=['PUT'])
|
||||
@jwt_to_session_cookie
|
||||
@login_required
|
||||
def api_fitness_fitness_goals_history():
|
||||
goals = json.loads(request.stream.read())
|
||||
goals["player_id"] = current_user.player_id
|
||||
goals["lastUpdated"] = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + 'Z'
|
||||
db.session.add(GoalMetrics(**goals))
|
||||
db.session.commit()
|
||||
return '', 204
|
||||
|
||||
|
||||
@app.teardown_request
|
||||
def teardown_request(exception):
|
||||
@@ -3953,6 +4103,7 @@ with app.app_context():
|
||||
if db.session.execute(sqlalchemy.text("SELECT COUNT(*) FROM pragma_table_info('user') WHERE name='new_home'")).scalar():
|
||||
db.session.execute(sqlalchemy.text("ALTER TABLE user DROP COLUMN new_home"))
|
||||
db.session.commit()
|
||||
check_columns(Activity, 'activity')
|
||||
if check_columns(Playback, 'playback'):
|
||||
update_playback()
|
||||
check_columns(RouteResult, 'route_result')
|
||||
|
||||
Reference in New Issue
Block a user