diff --git a/.gitignore b/.gitignore index 0787bc0..fcee6fe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ *.pyc storage/* !storage/force_dir_in_git.txt -example-requests/* \ No newline at end of file +example-requests/* +zoffline.log +migrate_auth.sql \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 61418f7..dd04fd8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ MAINTAINER zoffline WORKDIR /usr/src/app RUN apk add --no-cache git -RUN pip install flask protobuf protobuf3_to_dict stravalib garmin-uploader +RUN pip install flask flask_sqlalchemy flask-login pyjwt protobuf protobuf3_to_dict stravalib RUN git clone --depth 1 https://github.com/zoffline/zwift-offline RUN chmod 777 zwift-offline/storage diff --git a/cdn/static/web/launcher/signup.html b/cdn/static/web/launcher/signup.html index 4be58ae..3c617f7 100644 --- a/cdn/static/web/launcher/signup.html +++ b/cdn/static/web/launcher/signup.html @@ -10,41 +10,39 @@
-

Sign up

+

Sign up

- +
-
+
- +
-
-
-
+
- +
- +
- +
- Back + Back
@@ -52,7 +50,7 @@ {% if messages %}
    {% for message in messages %} -
  • {{ message }}
  • +
  • {{ message }}
  • {% endfor %}
{% endif %} diff --git a/cdn/static/web/launcher/upload.html b/cdn/static/web/launcher/upload.html index 8590cbe..74c6009 100644 --- a/cdn/static/web/launcher/upload.html +++ b/cdn/static/web/launcher/upload.html @@ -18,7 +18,11 @@ profile.bin
- {{ profile }} + {% if profile %} + {{ profile }} + {% else %} + {{ profile }} + {% endif %}
diff --git a/protobuf/segment-result.proto b/protobuf/segment-result.proto index 19acf4d..0ad5d8a 100644 --- a/protobuf/segment-result.proto +++ b/protobuf/segment-result.proto @@ -12,9 +12,9 @@ message SegmentResult { optional string finish_time_str = 10; required uint64 elapsed_ms = 11; optional bool f12 = 12; - optional uint32 f13 = 13; + optional uint32 f13 = 13; //weight_in_grams optional uint32 f14 = 14; - optional uint32 f15 = 15; + optional uint32 f15 = 15; //avg_power optional bool f16 = 16; optional string f17 = 17; optional uint64 f18 = 18; diff --git a/scripts/gen_schedule.py b/scripts/gen_schedule.py index 7a48154..9bca5a6 100755 --- a/scripts/gen_schedule.py +++ b/scripts/gen_schedule.py @@ -16,7 +16,7 @@ MAPS = [ 'FRANCE' ] + [ 'INNSBRUCK' ] + [ 'LONDON' ] * 2 + [ 'NEWYORK' ] * 2 + [ dom = minidom.parseString('1') appts = dom.getElementsByTagName('appointments')[0] -now = datetime.datetime.now() +now = datetime.datetime.utcnow() prev_map = None for i in range(0, 500): map_choice = random.choice(MAPS) diff --git a/standalone.py b/standalone.py index ac62c9d..7349fcf 100755 --- a/standalone.py +++ b/standalone.py @@ -7,6 +7,7 @@ import sys import threading import time import csv +from collections import deque from datetime import datetime from shutil import copyfile if sys.version_info[0] > 2: @@ -35,7 +36,7 @@ else: PROXYPASS_FILE = "%s/cdn-proxy.txt" % STORAGE_DIR SERVER_IP_FILE = "%s/server-ip.txt" % STORAGE_DIR -MAP_OVERRIDE = None +MAP_OVERRIDE = deque(maxlen=16) update_freq = 3 globalGhosts = {} @@ -69,7 +70,7 @@ def saveGhost(name, player_id): os.makedirs(folder) except: return - f = '%s/%s-%s.bin' % (folder, time.strftime("%Y-%m-%d-%H-%M-%S"), name) + f = '%s/%s-%s.bin' % (folder, zwift_offline.getUTCDateTime().strftime("%Y-%m-%d-%H-%M-%S"), name) with open(f, 'wb') as fd: fd.write(ghosts.rec.SerializeToString()) @@ -152,23 +153,16 @@ class CDNHandler(SimpleHTTPRequestHandler): return fullpath def do_GET(self): - global MAP_OVERRIDE path_end = self.path.rsplit('/', 1)[1] if path_end in ['FRANCE', 'INNSBRUCK', 'LONDON', 'NEWYORK', 'PARIS', 'RICHMOND', 'WATOPIA', 'YORKSHIRE']: - MAP_OVERRIDE = path_end + # We have no identifying information when Zwift makes MapSchedule request except for the client's IP. + MAP_OVERRIDE.append((self.client_address[0], path_end)) self.send_response(302) + self.send_header('Cookie', self.headers.get('Cookie') + "; map=%s" % path_end) self.send_header('Location', 'https://secure.zwift.com/ride') self.end_headers() return - if MAP_OVERRIDE and self.path == '/gameassets/MapSchedule_v2.xml': - self.send_response(200) - self.send_header('Content-type', 'text/xml') - self.end_headers() - output = '1' % (MAP_OVERRIDE, datetime.now().strftime("%Y-%m-%dT00:01-04")) - self.wfile.write(output.encode()) - MAP_OVERRIDE = None - return - elif self.path == '/gameassets/MapSchedule_v2.xml' and os.path.exists(PROXYPASS_FILE): + if self.path == '/gameassets/MapSchedule_v2.xml' and os.path.exists(PROXYPASS_FILE): # PROXYPASS_FILE existence indicates we know what we're doing and # we can try to obtain the official map schedule. This can only work # if we're running on a different machine than the Zwift client. @@ -183,6 +177,17 @@ class CDNHandler(SimpleHTTPRequestHandler): return except: pass # fallthrough to return zoffline version + elif self.path == '/gameassets/MapSchedule_v2.xml': + # Check if client requested the map be overridden + for override in MAP_OVERRIDE: + if override[0] == self.client_address[0]: + self.send_response(200) + self.send_header('Content-type', 'text/xml') + self.end_headers() + output = '1' % (override[1], datetime.now().strftime("%Y-%m-%dT00:01-04")) + self.wfile.write(output.encode()) + MAP_OVERRIDE.remove(override) + return SimpleHTTPRequestHandler.do_GET(self) @@ -243,7 +248,7 @@ class TCPHandler(socketserver.BaseRequestHandler): msg.f11 = 1 payload = msg.SerializeToString() - lastAliveCheck = int(time.time()) + lastAliveCheck = int(zwift_offline.getUTCTime()) while True: #Check every 5 seconds for new updates tcpthreadevent.wait(timeout=5) @@ -259,11 +264,23 @@ class TCPHandler(socketserver.BaseRequestHandler): for player_update_proto in playerUpdateQueue[player_id]: player_update = message.updates.add() player_update.ParseFromString(player_update_proto) + + #Send if 10 updates has already been added and start a new message + if len(message.updates) > 9: + message_payload = message.SerializeToString() + self.request.sendall(struct.pack('!h', len(message_payload))) + self.request.sendall(message_payload) + + message = udp_node_msgs_pb2.ServerToClient() + message.f1 = 1 + message.player_id = player_id + message.world_time = zwift_offline.world_time() + added_player_updates.append(player_update_proto) for player_update_proto in added_player_updates: playerUpdateQueue[player_id].remove(player_update_proto) - t = int(time.time()) + t = int(zwift_offline.getUTCTime()) #Check if any updates are added and should be sent to client, otherwise just keep alive every 25 seconds if len(message.updates) > 0: @@ -276,7 +293,7 @@ class TCPHandler(socketserver.BaseRequestHandler): self.request.sendall(struct.pack('!h', len(payload))) self.request.sendall(payload) except Exception as e: - print('Exception: %s' % e) + print('Exception TCP: %s' % e) break class GhostsVariables: @@ -325,7 +342,7 @@ class UDPHandler(socketserver.BaseRequestHandler): ghosts.rec.player_id = player_id organizeGhosts(player_id) - t = int(time.time()) + t = int(zwift_offline.getUTCTime()) ghosts.lastPackageTime = t if player_id in ghostsEnabled and ghostsEnabled[player_id]: diff --git a/zwift_offline.py b/zwift_offline.py index ce051fb..91007a8 100644 --- a/zwift_offline.py +++ b/zwift_offline.py @@ -18,7 +18,7 @@ from io import BytesIO from shutil import copyfile import jwt -from flask import Flask, request, jsonify, g, redirect, render_template, url_for, flash, session, abort +from flask import Flask, request, jsonify, g, redirect, render_template, url_for, flash, session, abort, make_response, send_file from flask_login import UserMixin, AnonymousUserMixin, LoginManager, login_user, current_user, login_required from google.protobuf.descriptor import FieldDescriptor from protobuf_to_dict import protobuf_to_dict, TYPE_CALLABLE_MAP @@ -37,8 +37,7 @@ import protobuf.world_pb2 as world_pb2 import protobuf.zfiles_pb2 as zfiles_pb2 import protobuf.hash_seeds_pb2 as hash_seeds_pb2 - -logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO")) +logging.basicConfig(filename='zoffline.log', level=os.environ.get("LOGLEVEL", "INFO")) logger = logging.getLogger('zoffline') logger.setLevel(logging.WARN) @@ -78,11 +77,9 @@ if os.path.exists("%s/multiplayer.txt" % STORAGE_DIR): MULTIPLAYER = True from tokens import * -AUTH_PATH = "%s/auth.db" % STORAGE_DIR - # Android uses https for cdn app = Flask(__name__, static_folder='%s/cdn/gameassets' % SCRIPT_DIR, static_url_path='/gameassets', template_folder='%s/cdn/static/web/launcher' % SCRIPT_DIR) -app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///{db}'.format(db=AUTH_PATH) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///{db}'.format(db=DATABASE_PATH) app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False if not os.path.exists(SECRET_KEY_FILE): with open(SECRET_KEY_FILE, 'wb') as f: @@ -152,6 +149,12 @@ coursesLookup = { 15: 'Paris' } +def getUTCDateTime(): + return datetime.datetime.utcnow() + +def getUTCTime(): + return getUTCDateTime().timestamp() + def getOnline(): onlineInRegion = Online() for p_id in online: @@ -337,6 +340,16 @@ def upload(username): return render_template("upload.html", username=current_user.username, profile=profile, name=name, token=token) +@app.route("/download/profile.bin", methods=["GET"]) +@login_required +def download(): + player_id = current_user.player_id + profile_dir = os.path.join(STORAGE_DIR, str(player_id)) + profile_file = os.path.join(profile_dir, 'profile.bin') + if os.path.isfile(profile_file): + return send_file(profile_file, attachment_filename='profile.bin') + + @app.route("/logout/") def logout(username): flash("Successfully logged out.") @@ -353,13 +366,11 @@ type_callable_map[FieldDescriptor.TYPE_UINT64] = str def insert_protobuf_into_db(table_name, msg): - cur = g.db.cursor() msg_dict = protobuf_to_dict(msg, type_callable_map=type_callable_map) columns = ', '.join(list(msg_dict.keys())) placeholders = ':'+', :'.join(list(msg_dict.keys())) query = 'INSERT INTO %s (%s) VALUES (%s)' % (table_name, columns, placeholders) - cur.execute(query, msg_dict) - g.db.commit() + db.engine.execute(query, msg_dict) # XXX: can't be used to 'nullify' a column value @@ -371,14 +382,12 @@ def update_protobuf_in_db(table_name, msg, id): id = str(id) except AttributeError: pass - cur = g.db.cursor() msg_dict = protobuf_to_dict(msg, type_callable_map=type_callable_map) columns = ', '.join(list(msg_dict.keys())) placeholders = ':'+', :'.join(list(msg_dict.keys())) setters = ', '.join('{}=:{}'.format(key, key) for key in msg_dict) query = 'UPDATE %s SET %s WHERE id=%s' % (table_name, setters, id) - cur.execute(query, msg_dict) - g.db.commit() + db.engine.execute(query, msg_dict) def row_to_protobuf(row, msg, exclude_fields=[]): @@ -397,19 +406,18 @@ def row_to_protobuf(row, msg, exclude_fields=[]): # FIXME: I should really do this properly... def get_id(table_name): - cur = g.db.cursor() while True: # I think activity id is actually only uint32. On the off chance it's # int32, stick with 31 bits. ident = int(random.getrandbits(31)) - cur.execute("SELECT id FROM %s WHERE id = ?" % table_name, (str(ident),)) - if not cur.fetchall(): + row = db.engine.execute("SELECT id FROM %s WHERE id = %s" % (table_name, ident)).first() + if not row: break return ident def world_time(): - return int((time.time()-1414016075)*1000) + return int((getUTCTime()-1414016075)*1000) @app.route('/api/auth', methods=['GET']) @@ -425,7 +433,7 @@ def api_users_login(): response.info.relay_url = "https://us-or-rly101.zwift.com/relay" response.info.apis.todaysplan_url = "https://whats.todaysplan.com.au" response.info.apis.trainingpeaks_url = "https://api.trainingpeaks.com" - response.info.time = int(time.time()) + response.info.time = int(getUTCTime()) udp_node = response.info.nodes.node.add() if os.path.exists(SERVER_IP_FILE): with open(SERVER_IP_FILE, 'r') as f: @@ -473,7 +481,7 @@ def api_zfiles(): zfile.id = int(random.getrandbits(31)) zfile.folder = "logfiles" zfile.filename = "yep_took_good_care_of_that_file.txt" - zfile.timestamp = int(time.time()) + zfile.timestamp = int(getUTCTime()) return zfile.SerializeToString(), 200 @@ -540,11 +548,9 @@ def api_profiles_me(): # However, without it, anyone "upgrading" to multiplayer mode will lose their existing data. # TODO: need a warning in README that switching to multiplayer mode and back to single player will lose your existing data. if profile.id != profile_id: - cur = g.db.cursor() - cur.execute('UPDATE activity SET player_id = ? WHERE player_id = ?', (str(profile_id), str(profile.id))) - cur.execute('UPDATE goal SET player_id = ? WHERE player_id = ?', (str(profile_id), str(profile.id))) - cur.execute('UPDATE segment_result SET player_id = ? WHERE player_id = ?', (str(profile_id), str(profile.id))) - g.db.commit() + db.engine.execute('UPDATE activity SET player_id = ? WHERE player_id = ?', (str(profile_id), str(profile.id))) + db.engine.execute('UPDATE goal SET player_id = ? WHERE player_id = ?', (str(profile_id), str(profile.id))) + db.engine.execute('UPDATE segment_result SET player_id = ? WHERE player_id = ?', (str(profile_id), str(profile.id))) profile.id = profile_id elif current_user.player_id != profile.id: # Update AnonUser's player_id to match @@ -594,10 +600,9 @@ def api_profiles_activities(player_id): # request.method == 'GET' activities = activity_pb2.Activities() - cur = g.db.cursor() # Select every column except 'fit' - despite being a blob python 3 treats it like a utf-8 string and tries to decode it - cur.execute("SELECT id, player_id, f3, name, f5, f6, start_date, end_date, distance, avg_heart_rate, max_heart_rate, avg_watts, max_watts, avg_cadence, max_cadence, avg_speed, max_speed, calories, total_elevation, strava_upload_id, strava_activity_id, f23, fit_filename, f29, date FROM activity WHERE player_id = ?", (str(player_id),)) - for row in cur.fetchall(): + rows = db.engine.execute("SELECT id, player_id, f3, name, f5, f6, start_date, end_date, distance, avg_heart_rate, max_heart_rate, avg_watts, max_watts, avg_cadence, max_cadence, avg_speed, max_speed, calories, total_elevation, strava_upload_id, strava_activity_id, f23, fit_filename, f29, date FROM activity WHERE player_id = ?", (str(player_id),)) + for row in rows: activity = activities.activities.add() row_to_protobuf(row, activity, exclude_fields=['fit']) a = activity @@ -606,7 +611,7 @@ def api_profiles_activities(player_id): #a.avg_watts == 0 and a.calories == 0 and a.distance == 0 and a.max_cadence == 0 and #a.max_heart_rate == 0 and a.max_speed == 0 and a.max_watts == 0): if a.distance == 0: - cur.executescript("DELETE FROM activity WHERE id = %s" % a.id) + db.engine.execute("DELETE FROM activity WHERE id = %s" % a.id) activities.activities.remove(a) return activities.SerializeToString(), 200 @@ -665,7 +670,7 @@ def strava_upload(player_id, activity): logger.warn("Failed to read %s/strava_token.txt. Skipping Strava upload attempt." % profile_dir) return try: - if time.time() > int(expires_at): + if getUTCTime() > int(expires_at): refresh_response = strava.refresh_access_token(client_id=client_id, client_secret=client_secret, refresh_token=refresh_token) with open('%s/strava_token.txt' % profile_dir, 'w') as f: @@ -755,7 +760,7 @@ def api_profiles_activities_rideon(recieving_player_id): player_update.type = 4 #ride on type player_update.world_time1 = world_time() player_update.world_time2 = player_update.world_time1 + 9890 - player_update.f14 = int(time.time() * 1000000) + player_update.f14 = int(getUTCTime() * 1000000) ride_on = udp_node_msgs_pb2.RideOn() ride_on.rider_id = int(sending_player_id) @@ -796,24 +801,22 @@ def get_month_range(dt): def unix_time_millis(dt): - return int(dt.strftime('%s')) * 1000 + return int(dt.timestamp()*1000) def fill_in_goal_progress(goal, player_id): - cur = g.db.cursor() - now = datetime.datetime.now() + now = getUTCDateTime() if goal.periodicity == 0: # weekly first_dt, last_dt = get_week_range(now) else: # monthly first_dt, last_dt = get_month_range(now) if goal.type == 0: # distance - cur.execute("""SELECT SUM(distance) FROM activity + distance = db.engine.execute("""SELECT SUM(distance) FROM activity WHERE player_id = ? AND strftime('%s', start_date) >= strftime('%s', ?) AND strftime('%s', start_date) <= strftime('%s', ?) AND end_date IS NOT NULL""", - (str(player_id), first_dt, last_dt)) - distance = cur.fetchall()[0][0] + (str(player_id), first_dt, last_dt)).first()[0] if distance: goal.actual_distance = distance goal.actual_duration = distance @@ -822,14 +825,13 @@ def fill_in_goal_progress(goal, player_id): goal.actual_duration = 0.0 else: # duration - cur.execute("""SELECT SUM(julianday(end_date) - julianday(start_date)) + duration = db.engine.execute("""SELECT SUM(julianday(end_date) - julianday(start_date)) FROM activity WHERE player_id = ? AND strftime('%s', start_date) >= strftime('%s', ?) AND strftime('%s', start_date) <= strftime('%s', ?) AND end_date IS NOT NULL""", - (str(player_id), first_dt, last_dt)) - duration = cur.fetchall()[0][0] + (str(player_id), first_dt, last_dt)).first()[0] if duration: goal.actual_duration = duration*1440 # convert from days to minutes goal.actual_distance = duration*1440 @@ -857,7 +859,7 @@ def api_profiles_goals(player_id): goal = goal_pb2.Goal() goal.ParseFromString(request.stream.read()) goal.id = get_id('goal') - now = datetime.datetime.now() + now = getUTCDateTime() goal.created_on = unix_time_millis(now) set_goal_end_date(goal, now) fill_in_goal_progress(goal, player_id) @@ -867,14 +869,12 @@ def api_profiles_goals(player_id): # request.method == 'GET' goals = goal_pb2.Goals() - cur = g.db.cursor() - cur.execute("SELECT * FROM goal WHERE player_id = ?", (str(player_id),)) - rows = cur.fetchall() + rows = db.engine.execute("SELECT * FROM goal WHERE player_id = ?", (str(player_id),)) for row in rows: goal = goals.goals.add() row_to_protobuf(row, goal) end_dt = datetime.datetime.fromtimestamp(goal.period_end_date / 1000) - now = datetime.datetime.now() + now = getUTCDateTime() if end_dt < now: set_goal_end_date(goal, now) update_protobuf_in_db('goal', goal, goal.id) @@ -890,9 +890,7 @@ def api_profiles_goals_id(player_id, goal_id): if player_id != current_user.player_id: return '', 401 goal_id = int(goal_id) & 0xffffffffffffffff - cur = g.db.cursor() - cur.execute("DELETE FROM goal WHERE id = ?", (str(goal_id),)) - g.db.commit() + db.engine.execute("DELETE FROM goal WHERE id = ?", (str(goal_id),)) return '', 200 @@ -923,7 +921,7 @@ def relay_worlds_generic(world_id=None): #serializedMessage = chat_message.SerializeToString() except: #Not able to decode as playerupdate, send dummy response - world = { 'currentDateTime': int(time.time()), + world = { 'currentDateTime': int(getUTCTime()), 'currentWorldTime': world_time(), 'friendsInWorld': [], 'mapId': 1, @@ -940,7 +938,7 @@ def relay_worlds_generic(world_id=None): #PlayerUpdate player_update.world_time2 = world_time() + 60000 player_update.f12 = 1 - player_update.f14 = int(str(int(time.time()*1000000))) + player_update.f14 = int(str(int(getUTCTime()*1000000))) for recieving_player_id in online.keys(): should_receive = False if player_update.type == 5 or player_update.type == 105: @@ -983,7 +981,7 @@ def relay_worlds_generic(world_id=None): world.name = 'Public Watopia' world.f3 = course world.world_time = world_time() - world.real_time = int(time.time()) + world.real_time = int(getUTCTime()) playersInRegion = 0 for p_id in online.keys(): player = online[p_id] @@ -1084,16 +1082,13 @@ def relay_periodic_info(): return infos.SerializeToString(), 200 -def add_segment_results(segment_id, player_id, only_best, from_date, to_date, results, only_own): - cur = g.db.cursor() +def add_segment_results(segment_id, player_id, only_best, from_date, to_date, results): where_stmt = "WHERE segment_id = ?" where_args = [str(segment_id)] - if only_own and player_id: + rows = None + if player_id: where_stmt += " AND player_id = ?" where_args.append(player_id) - elif not only_own and player_id: - where_stmt += " AND player_id != ?" - where_args.append(player_id) if from_date: where_stmt += " AND strftime('%s', finish_time_str) > strftime('%s', ?)" where_args.append(from_date) @@ -1101,9 +1096,18 @@ def add_segment_results(segment_id, player_id, only_best, from_date, to_date, re where_stmt += " AND strftime('%s', finish_time_str) < strftime('%s', ?)" where_args.append(to_date) if only_best: - where_stmt += " ORDER BY elapsed_ms LIMIT 1" - cur.execute("SELECT * FROM segment_result %s" % where_stmt, where_args) - for row in cur.fetchall(): + where_stmt += " AND world_time > ?" + #Only include results from max 1 hour ago + where_args.append(world_time()-(60*60*1000)) + rows = db.engine.execute("""SELECT s1.* FROM segment_result s1 + JOIN (SELECT s.player_id, MIN(Cast(s.elapsed_ms AS INTEGER)) AS min_time + FROM segment_result s %s GROUP BY s.player_id) s2 ON s2.player_id = s1.player_id AND s2.min_time = CAST(s1.elapsed_ms AS INTEGER) + GROUP BY s1.player_id, s1.elapsed_ms + ORDER BY CAST(s1.elapsed_ms AS INTEGER) + LIMIT 1000""" % where_stmt, where_args) + else: + rows = db.engine.execute("SELECT * FROM segment_result %s" % where_stmt, where_args) + for row in rows: result = results.segment_results.add() row_to_protobuf(row, result, ['f3', 'f4', 'segment_id', 'event_subgroup_id', 'finish_time_str', 'f14', 'f17', 'f18']) @@ -1115,7 +1119,7 @@ def handle_segment_results(request): result.ParseFromString(request.stream.read()) result.id = get_id('segment_result') result.world_time = world_time() - result.finish_time_str = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ") + result.finish_time_str = getUTCDateTime().strftime("%Y-%m-%dT%H:%M:%SZ") result.f20 = 0 insert_protobuf_into_db('segment_result', result) return '{"id": %ld}' % result.id, 200 @@ -1138,12 +1142,10 @@ def handle_segment_results(request): if player_id: #Add players results - add_segment_results(segment_id, player_id, only_best, from_date, to_date, results, True) - #Add top 100 other players results - #add_segment_results(segment_id, player_id, only_best, from_date, to_date, results, False) + add_segment_results(segment_id, player_id, only_best, from_date, to_date, results) else: - #Jersey, only 1 result, player_id = None - add_segment_results(segment_id, player_id, only_best, from_date, to_date, results, False) + #Top 100 results, player_id = None + add_segment_results(segment_id, player_id, only_best, from_date, to_date, results) return results.SerializeToString(), 200 @@ -1176,63 +1178,73 @@ def connect_db(): return conn -@app.before_request -def before_request(): - g.db = connect_db() - - @app.teardown_request def teardown_request(exception): - if hasattr(g, 'db'): - g.db.close() + if exception != None: + print('Exception: %s' % exception) def init_database(): - conn = connect_db() - cur = conn.cursor() if not os.path.exists(DATABASE_PATH) or not os.path.getsize(DATABASE_PATH): # Create a new database with open(DATABASE_INIT_SQL, 'r') as f: - cur.executescript(f.read()) - cur.execute('INSERT INTO version VALUES (?)', (DATABASE_CUR_VER,)) - conn.close() + db.engine.execute(f.read()) + db.engine.execute('INSERT INTO version VALUES (?)', (DATABASE_CUR_VER,)) return # Migrate database if necessary if not os.access(DATABASE_PATH, os.W_OK): logging.error("zwift-offline.db is not writable. Unable to upgrade database!") return - cur_version = cur.execute('SELECT version FROM version') - version = cur.fetchall()[0][0] + version = db.engine.execute('SELECT version FROM version').first()[0] if version == DATABASE_CUR_VER: - conn.close() return # Database needs to be upgraded, try to back it up first try: # Try writing to storage dir - copyfile(DATABASE_PATH, "%s.v%d.%d.bak" % (DATABASE_PATH, version, int(time.time()))) + copyfile(DATABASE_PATH, "%s.v%d.%d.bak" % (DATABASE_PATH, version, int(getUTCTime()))) except: try: # Fall back to a temporary dir - copyfile(DATABASE_PATH, "%s/zwift-offline.db.v%s.%d.bak" % (tempfile.gettempdir(), version, int(time.time()))) + copyfile(DATABASE_PATH, "%s/zwift-offline.db.v%s.%d.bak" % (tempfile.gettempdir(), version, int(getUTCTime()))) except: logging.warn("Failed to create a zoffline database backup prior to upgrading it.") if version < 1: # Adjust old world_time values in segment results to new rough estimate of Zwift's logging.info("Upgrading zwift-offline.db to version 2") - cur.execute('UPDATE segment_result SET world_time = world_time-1414016075000') - cur.execute('UPDATE version SET version = 2') + db.engine.execute('UPDATE segment_result SET world_time = world_time-1414016075000') + db.engine.execute('UPDATE version SET version = 2') if version == 1: logging.info("Upgrading zwift-offline.db to version 2") - cur.execute('UPDATE segment_result SET world_time = cast(world_time/64.4131403573055-1414016075 as int)*1000') - cur.execute('UPDATE version SET version = 2') + db.engine.execute('UPDATE segment_result SET world_time = cast(world_time/64.4131403573055-1414016075 as int)*1000') + db.engine.execute('UPDATE version SET version = 2') - conn.commit() - conn.close() + +def check_columns(): + time.sleep(3) + rows = db.engine.execute(sqlalchemy.text("PRAGMA table_info(user)")) + should_have_columns = User.metadata.tables['user'].columns + current_columns = list() + for row in rows: + current_columns.append(row[1]) + for column in should_have_columns: + if not column.name in current_columns: + nulltext = None + if column.nullable: + nulltext = "NULL" + else: + nulltext = "NOT NULL" + defaulttext = None + if column.default == None: + defaulttext = "" + else: + defaulttext = " DEFAULT %s" % column.default.arg + db.engine.execute(sqlalchemy.text("ALTER TABLE user ADD %s %s %s%s;" % (column.name, str(column.type), nulltext, defaulttext))) @app.before_first_request def before_first_request(): init_database() + check_columns() db.create_all() @@ -1320,6 +1332,7 @@ def auth_realms_zwift_protocol_openid_connect_token(): return FAKE_JWT, 200 @app.route("/start-zwift" , methods=['POST']) +@login_required def start_zwift(): if MULTIPLAYER: current_user.enable_ghosts = 'enableghosts' in request.form.keys() @@ -1331,7 +1344,9 @@ def start_zwift(): if selected_map == 'CALENDAR': return redirect("/ride", 302) else: - return redirect("http://cdn.zwift.com/%s" % selected_map, 302) + response = make_response(redirect("http://cdn.zwift.com/%s" % selected_map, 302)) + response.set_cookie('remember_token', request.cookies['remember_token'], domain=".zwift.com") + return response # Called by Mac, but not Windows