Add fitness service goals

This commit is contained in:
oldnapalm
2025-04-22 08:24:34 -03:00
parent f21f8548f2
commit b6e2c54303
10 changed files with 340 additions and 62 deletions

View File

@@ -1447,6 +1447,7 @@
},
{
"name": "game_1_86_fitness-service_goals",
"value": true,
"values": {
"fields": {
"enable_KJ_cal_SP": {

View File

@@ -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

View File

@@ -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
View 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
View 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)

View File

@@ -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

View File

@@ -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

View File

@@ -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')