mirror of
https://github.com/zoffline/zwift-offline.git
synced 2026-08-03 01:15:46 -07:00
Merge pull request #9 from Argon2000/mp
This commit is contained in:
+3
-1
@@ -1,4 +1,6 @@
|
||||
*.pyc
|
||||
storage/*
|
||||
!storage/force_dir_in_git.txt
|
||||
example-requests/*
|
||||
example-requests/*
|
||||
zoffline.log
|
||||
migrate_auth.sql
|
||||
+1
-1
@@ -4,7 +4,7 @@ MAINTAINER zoffline <zoffline@tutanota.com>
|
||||
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
|
||||
|
||||
@@ -10,41 +10,39 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<h1>Sign up</h1>
|
||||
<h3>Sign up</h2>
|
||||
<form id="signup" action="{{ url_for('signup') }}" method="post">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<label>Username</label>
|
||||
<input type="text" id="username" name="username" class="form-control">
|
||||
<input type="text" id="username" name="username" class="form-control form-control-sm">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="col-xs-4 col-sm-4 col-md-4">
|
||||
<label>Password</label>
|
||||
<input type="password" id="password" name="password" class="form-control">
|
||||
<input type="password" id="password" name="password" class="form-control form-control-sm">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="col-xs-4 col-sm-4 col-md-4">
|
||||
<label>Confirm Password</label>
|
||||
<input type="password" id="confirm_password" name="confirm_password" class="form-control">
|
||||
<input type="password" id="confirm_password" name="confirm_password" class="form-control form-control-sm">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<label>First name</label>
|
||||
<input type="text" id="first_name" name="first_name" class="form-control">
|
||||
<input type="text" id="first_name" name="first_name" class="form-control form-control-sm">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<label>Last name</label>
|
||||
<input type="text" id="last_name" name="last_name" class="form-control">
|
||||
<input type="text" id="last_name" name="last_name" class="form-control form-control-sm">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 top-buffer">
|
||||
<input type="submit" value="Sign up" class="btn btn-outline-primary"><a href="{{ url_for('login') }}" class="btn btn-outline-danger">Back</a>
|
||||
<input type="submit" value="Sign up" class="btn btn-sm btn-outline-primary"><a href="{{ url_for('login') }}" class="btn btn-sm btn-outline-danger">Back</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -52,7 +50,7 @@
|
||||
{% if messages %}
|
||||
<ul class="list-group top-buffer">
|
||||
{% for message in messages %}
|
||||
<li class="list-group-item">{{ message }}</li>
|
||||
<li class="list-group-item py-2">{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
@@ -18,7 +18,11 @@
|
||||
profile.bin
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
{{ profile }}
|
||||
{% if profile %}
|
||||
<a href="/download/profile.bin">{{ profile }}</a>
|
||||
{% else %}
|
||||
{{ profile }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -16,7 +16,7 @@ MAPS = [ 'FRANCE' ] + [ 'INNSBRUCK' ] + [ 'LONDON' ] * 2 + [ 'NEWYORK' ] * 2 + [
|
||||
dom = minidom.parseString('<MapSchedule><appointments></appointments><VERSION>1</VERSION></MapSchedule>')
|
||||
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)
|
||||
|
||||
+34
-17
@@ -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 = '<MapSchedule><appointments><appointment map="%s" start="%s"/></appointments><VERSION>1</VERSION></MapSchedule>' % (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 = '<MapSchedule><appointments><appointment map="%s" start="%s"/></appointments><VERSION>1</VERSION></MapSchedule>' % (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]:
|
||||
|
||||
+103
-88
@@ -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/<username>")
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user