mirror of
https://github.com/zoffline/zwift-offline.git
synced 2026-08-03 01:15:46 -07:00
Merge pull request #29 from zoffline/master
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
# Only breaking changes in releases will be documented here.
|
||||
|
||||
Update to Zwift_1.0.60239:
|
||||
* With the update to this Zwift release, zoffline has included significant changes
|
||||
to fully support a multiplayer mode, pace partners, and more.
|
||||
* New dependencies required if running from source: pyJWT, flask-login, FlaskSQLAlchemy, gevent
|
||||
* Install these with ``pip install pyjwt flask-login flask_sqlalchemy gevent``
|
||||
* Multi-profile support has been deprecated in favor of full multiplayer support.
|
||||
* See Step 6 in README.md for how to enable multiplayer support.
|
||||
* Create an account for each profile you had and upload their profile.bin
|
||||
and credential files via the new Zwift launcher.
|
||||
* Do not "upgrade" from an existing single player installation to multiplayer and then
|
||||
"downgrade" back to single player without backing up your original storage directory.
|
||||
You will lose your activities, segment results, and goals if you do this and don't
|
||||
have a backup to restore from.
|
||||
|
||||
|
||||
Update to Zwift_1.0.51959:
|
||||
* Changes were made to Zwift's cacert.pem. You will likely need to re-append
|
||||
cert-zwift-com.pem to Zwift's cacert.pem in a specific way.
|
||||
See https://github.com/zoffline/zwift-offline/issues/62 for details.
|
||||
+1
-1
@@ -3,7 +3,7 @@ MAINTAINER zoffline <zoffline@tutanota.com>
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apk add --no-cache git
|
||||
RUN apk add --no-cache git gcc musl-dev libffi-dev openssl-dev file make
|
||||
RUN pip install flask flask_sqlalchemy flask-login pyjwt gevent protobuf protobuf3_to_dict stravalib garmin-uploader cryptography
|
||||
|
||||
RUN git clone --depth 1 https://github.com/zoffline/zwift-offline
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# zoffline
|
||||
|
||||
zoffline enables the use of [Zwift](http://zwift.com) offline by acting as a partial implementation of a Zwift server.
|
||||
zoffline enables the use of [Zwift](http://zwift.com) offline by acting as a partial implementation of a Zwift server. By default zoffline is only for a single player. See [Step 6: Enable Multiplayer](#step-6-optional-enable-multiplayer) for how to enable support for multiple users/profiles.
|
||||
|
||||
zoffline also offers riding against ghosts (your previous rides). Enable this feature by checking "Enable ghosts" in zoffline's launcher. See https://github.com/zoffline/zwift-offline/issues/56 for extra details.
|
||||
|
||||
@@ -27,19 +27,20 @@ To install zoffline on Windows:
|
||||
<details><summary>Linux, Windows, or Mac OS X (from source)</summary>
|
||||
To install zoffline on Linux, Windows, or Mac OS X:
|
||||
|
||||
* Install Python 2 or 3 (https://www.python.org/downloads/) if not already installed
|
||||
* Install Python 3 if not already installed
|
||||
* On Windows, installing Python via the Microsoft Store is highly recommend! If using a Python installer, ensure that in the first Python installer screen "Add Python 3.x to PATH" is checked.
|
||||
* Install dependencies: flask, flask_sqlalchemy, flask-login, pyjwt, gevent, python-protobuf, protobuf3_to_dict, stravalib (optional)
|
||||
* e.g., on Linux/Mac: ``pip install flask flask_sqlalchemy flask-login pyjwt gevent protobuf protobuf3_to_dict stravalib``
|
||||
* e.g., on Windows in command prompt: ``C:\Python27\Scripts\pip.exe install flask flask_sqlalchemy flask-login pyjwt gevent protobuf protobuf3_to_dict stravalib``
|
||||
* Python 3 is installed by default in ``C:\Users\<username>\AppData\Local\Programs\Python\Python38-32`` instead of ``C:\Python27``
|
||||
* e.g., on Windows in command prompt: ``pip install flask flask_sqlalchemy flask-login pyjwt gevent protobuf protobuf3_to_dict stravalib``
|
||||
* You may need to use ``C:\Users\<username>\AppData\Local\Programs\Python\Python39\Scripts\pip.exe`` instead of just ``pip``
|
||||
* Clone or download this repo
|
||||
* If you are not running zoffline on the same PC that Zwift is running: create a ``server-ip.txt`` file in the ``storage`` directory containing the IP address of the PC running zoffline.
|
||||
* Run standalone.py before starting Zwift
|
||||
* e.g., on Linux/Mac: ``sudo ./standalone.py``
|
||||
* sudo is needed because we're binding to the privileged ports 80 and 443.
|
||||
* If using Python 3, but Python 3 is not your system default run ``sudo python3 standalone.py``
|
||||
* e.g., on Windows in command prompt: ``C:\Python27\python.exe standalone.py``
|
||||
* For Python 3 the command will likely be ``C:\Users\<username>\AppData\Local\Programs\Python\Python38-32\python.exe standalone.py``
|
||||
* e.g., on Windows in command prompt: ``python standalone.py``
|
||||
* You may need to use ``C:\Users\<username>\AppData\Local\Programs\Python\Python39\python.exe`` instead of just ``python``
|
||||
* Start Zwift with standalone.py running (__after completing step 2__)
|
||||
* Note: When upgrading zoffline, be sure to retain the ``storage`` directory. It contains your Zwift progress state.
|
||||
|
||||
@@ -171,6 +172,8 @@ the Zwift system tray icon and exit) and restart Zwift.
|
||||
|
||||
### Step 3 [OPTIONAL]: Obtain current Zwift profile
|
||||
|
||||
<details><summary>Expand</summary>
|
||||
|
||||
If you don't obtain your current Zwift profile before first starting Zwift with
|
||||
zoffline enabled, you will be prompted to create a new profile (height, weight,
|
||||
gender). Your profile can be further customized and changed via the in game
|
||||
@@ -185,27 +188,35 @@ To obtain your current profile:
|
||||
* If multiplayer is enabled, use the upload button in the launcher window to import your file.
|
||||
* If using Docker, move profile.bin into the path you passed to ``-v``
|
||||
|
||||
</details>
|
||||
|
||||
### Step 4 [OPTIONAL]: Obtain Strava API token
|
||||
### Step 4 [OPTIONAL]: Upload activities to Strava
|
||||
|
||||
<details><summary>Expand</summary>
|
||||
|
||||
* Install dependencies: stravalib
|
||||
* e.g., on Linux/Mac: ``pip install stravalib``
|
||||
* e.g., on Windows in command prompt: ``C:\Python27\Scripts\pip.exe install stravalib``
|
||||
* Or, if using the Windows zoffline.exe version without Python installed you can run strava_auth.exe obtained from https://github.com/zoffline/zwift-offline/releases/tag/zoffline_helper in place of ``scripts/strava_auth.py`` below.
|
||||
* e.g., on Windows in command prompt: ``pip install stravalib``
|
||||
* You may need to use ``C:\Users\<username>\AppData\Local\Programs\Python\Python39\Scripts\pip.exe`` instead of just ``pip``
|
||||
* Or, if using the Windows zoffline.exe version without Python installed you can run ``strava_auth.exe`` obtained from https://github.com/zoffline/zwift-offline/releases/tag/zoffline_helper in place of ``scripts/strava_auth.py`` below.
|
||||
* [OPTIONAL] Get CLIENT_ID and CLIENT_SECRET from https://www.strava.com/settings/api
|
||||
* Run ``scripts/strava_auth.py --client-id CLIENT_ID --client-secret CLIENT_SECRET``
|
||||
* Run without arguments to use default values.
|
||||
* Open http://localhost:8000/ and authorize.
|
||||
* Move the resulting strava_token.txt (saved in whatever directory you ran strava_auth.py in) into the ``storage/<player_id>`` directory.
|
||||
* Move the resulting ``strava_token.txt`` (saved in whatever directory you ran ``strava_auth.py`` in) into the ``storage/<player_id>`` directory.
|
||||
* If multiplayer is enabled, use the upload button in the launcher window to import your file.
|
||||
|
||||
</details>
|
||||
|
||||
### Step 5 [OPTIONAL]: Upload activities to Garmin Connect
|
||||
|
||||
<details><summary>Expand</summary>
|
||||
|
||||
* Install dependencies: garmin-uploader, cryptography (optional)
|
||||
* e.g., on Linux/Mac: ``pip install garmin-uploader cryptography``
|
||||
* e.g., on Windows in command prompt: ``C:\Python27\Scripts\pip.exe install garmin-uploader cryptography``
|
||||
* Create a file garmin_credentials.txt in the ``storage/<player_id>`` directory containing your login credentials
|
||||
* e.g., on Windows in command prompt: ``pip install garmin-uploader cryptography``
|
||||
* You may need to use ``C:\Users\<username>\AppData\Local\Programs\Python\Python39\Scripts\pip.exe`` instead of just ``pip``
|
||||
* Create a file ``garmin_credentials.txt`` in the ``storage/<player_id>`` directory containing your login credentials
|
||||
```
|
||||
<username>
|
||||
<password>
|
||||
@@ -213,16 +224,27 @@ To obtain your current profile:
|
||||
* Note: this is not secure. Only do this if you are comfortable with your login credentials being stored in a clear text file.
|
||||
* If multiplayer is enabled, use the upload button in the launcher window to encrypt the credentials file.
|
||||
|
||||
</details>
|
||||
|
||||
### Step 6 [OPTIONAL]: Enable multiplayer
|
||||
|
||||
* Create a multiplayer.txt file in the ``storage`` directory.
|
||||
* If you are not running zoffline on the same PC that Zwift is running: create a server-ip.txt file in the ``storage`` directory containing the IP address of the PC running zoffline.
|
||||
<details><summary>Expand</summary>
|
||||
|
||||
<details><summary>Extra optional steps</summary>
|
||||
To enable support for multiple users perform the steps below. zoffline's previous multi-profile support has been superceded by full multiplayer support. If you were previously using multiple profiles with zoffline you will need to enable multiplayer to continue supporting multiple users.
|
||||
|
||||
* To obtain the official map schedule and update files from Zwift server: create a cdn-proxy.txt file in the ``storage`` directory. This can only work if you are running zoffline on a different machine than the Zwift client.
|
||||
* To enable the password reset feature: create a gmail_credentials.txt file in the ``storage`` directory containing the login credentials of a Gmail account. You need to enable the "Less secure app access" in the account settings and you may need to access https://accounts.google.com/DisplayUnlockCaptcha to allow the login from the server.
|
||||
* Create a ``multiplayer.txt`` file in the ``storage`` directory.
|
||||
* If you are not running zoffline on the same PC that Zwift is running: create a ``server-ip.txt`` file in the ``storage`` directory containing the IP address of the PC running zoffline.
|
||||
* TCP ports 80, 443, 3023 and UDP port 3022 will need to be open on the PC running zoffline if its running remotely.
|
||||
* Start Zwift and create an account in the new Zwift launcher and upload your ``profile.bin``, ``strava_credentials.txt``, and/or ``garmin_credentials.txt`` if you have them.
|
||||
* This account will only exist on your zoffline server and has no relation with your actual Zwift account.
|
||||
|
||||
</details>
|
||||
|
||||
### Extra optional steps
|
||||
<details><summary>Expand</summary>
|
||||
|
||||
* To obtain the official map schedule and update files from Zwift server: create a ``cdn-proxy.txt`` file in the ``storage`` directory. This can only work if you are running zoffline on a different machine than the Zwift client.
|
||||
* To enable the password reset feature when multiplayer is enabled: create a gmail_credentials.txt file in the ``storage`` directory containing the login credentials of a Gmail account. You need to enable the "Less secure app access" in the account settings and you may need to access https://accounts.google.com/DisplayUnlockCaptcha to allow the login from the server.
|
||||
* If the Zwift client is having issues connecting to the Linux server ("The request was aborted: Could not create SSL/TLS secure channel." or "The underlying connection was closed: An unexpected error occurred on a send. Received an unexpected EOF or 0 bytes from the transport stream."): change MinProtocol in /etc/ssl/openssl.cnf to TLSv1.0
|
||||
```
|
||||
[system_default_sect]
|
||||
@@ -231,9 +253,9 @@ To obtain your current profile:
|
||||
```
|
||||
</details>
|
||||
|
||||
## Discord and zoffline (online) server
|
||||
## Community Discord and zoffline (online) server
|
||||
|
||||
Please join the [Discord](https://discord.gg/GMdn8F8) and our enhanced version of zoffline, hosted Online!
|
||||
Please join the community supported [Discord](https://discord.gg/GMdn8F8) and their enhanced version of zoffline, hosted Online!
|
||||
|
||||
Follow the guide in #instructions to create your account and join other Zwifters.
|
||||
|
||||
@@ -243,7 +265,8 @@ Docker
|
||||
|
||||
-or-
|
||||
|
||||
* Python 2 or 3 (https://www.python.org/downloads/)
|
||||
* Python 3 (https://www.python.org/downloads/)
|
||||
* On Windows, installing Python via the Microsoft Store is highly recommend! If using a Python installer, ensure that in the first Python installer screen "Add Python 3.x to PATH" is checked.
|
||||
* Flask (http://flask.pocoo.org/)
|
||||
* ``pip install flask``
|
||||
* python-protobuf (https://pypi.org/project/protobuf/)
|
||||
@@ -269,7 +292,8 @@ Docker
|
||||
## Note
|
||||
|
||||
Future Zwift updates may break zoffline until it's updated. While zoffline is
|
||||
enabled Zwift updates will not be installed.
|
||||
enabled Zwift updates will not be installed. If a zoffline update broke
|
||||
something, check the ``CHANGELOG`` for possible changes that need to be made.
|
||||
|
||||
Don't expose zoffline to the internet, it was not designed with that in mind.
|
||||
|
||||
|
||||
+61
-43
@@ -3,36 +3,38 @@
|
||||
#
|
||||
# Adapted from https://github.com/jlemon/zlogger/blob/master/get_riders.py
|
||||
#
|
||||
#The MIT License (MIT)
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
#Copyright (c) 2016 Jonathan Lemon
|
||||
# Copyright (c) 2016 Jonathan Lemon
|
||||
#
|
||||
#Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
#of this software and associated documentation files (the "Software"), to deal
|
||||
#in the Software without restriction, including without limitation the rights
|
||||
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
#copies of the Software, and to permit persons to whom the Software is
|
||||
#furnished to do so, subject to the following conditions:
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
#The above copyright notice and this permission notice shall be included in all
|
||||
#copies or substantial portions of the Software.
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
#SOFTWARE.
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import sys, argparse, getpass
|
||||
import requests
|
||||
import argparse
|
||||
import getpass
|
||||
import json
|
||||
import os, time, stat
|
||||
from collections import namedtuple
|
||||
import os
|
||||
import requests
|
||||
import sys
|
||||
|
||||
|
||||
if getattr(sys, 'frozen', False):
|
||||
# If we're running as a pyinstaller bundle
|
||||
# If we're running as a py installer bundle
|
||||
SCRIPT_DIR = os.path.dirname(sys.executable)
|
||||
else:
|
||||
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||
@@ -45,6 +47,7 @@ except NameError:
|
||||
global args
|
||||
global dbh
|
||||
|
||||
|
||||
def post_credentials(session, username, password):
|
||||
# Credentials POSTing and tokens retrieval
|
||||
# POST https://secure.zwift.com/auth/realms/zwift/tokens/access/codes
|
||||
@@ -67,8 +70,8 @@ def post_credentials(session, username, password):
|
||||
"password": password,
|
||||
"grant_type": "password",
|
||||
},
|
||||
allow_redirects = False,
|
||||
verify = args.verifyCert,
|
||||
allow_redirects=False,
|
||||
verify=args.verifyCert,
|
||||
)
|
||||
|
||||
if args.verbose:
|
||||
@@ -84,6 +87,11 @@ def post_credentials(session, username, password):
|
||||
except requests.exceptions.RequestException as e:
|
||||
print('HTTP Request failed: %s' % e)
|
||||
|
||||
except KeyError as e:
|
||||
print('Invalid uname and/or password')
|
||||
exit(-1)
|
||||
|
||||
|
||||
def query_player_profile(session, access_token):
|
||||
# Query Player Profile
|
||||
# GET https://us-or-rly101.zwift.com/api/profiles/<player_id>
|
||||
@@ -99,7 +107,7 @@ def query_player_profile(session, access_token):
|
||||
"Authorization": "Bearer %s" % access_token,
|
||||
"Accept-Language": "en-us",
|
||||
},
|
||||
verify = args.verifyCert,
|
||||
verify=args.verifyCert,
|
||||
)
|
||||
|
||||
if args.verbose:
|
||||
@@ -131,7 +139,7 @@ def logout(session, refresh_token):
|
||||
"client_id": "Zwift_Mobile_Link",
|
||||
"refresh_token": refresh_token,
|
||||
},
|
||||
verify = args.verifyCert,
|
||||
verify=args.verifyCert,
|
||||
)
|
||||
if args.verbose:
|
||||
print('Response HTTP Status Code: {status_code}'.format(
|
||||
@@ -141,10 +149,12 @@ def logout(session, refresh_token):
|
||||
except requests.exceptions.RequestException as e:
|
||||
print('HTTP Request failed: %s' % e)
|
||||
|
||||
|
||||
def login(session, user, password):
|
||||
access_token, refresh_token, expired_in = post_credentials(session, user, password)
|
||||
return access_token, refresh_token
|
||||
|
||||
|
||||
def main(argv):
|
||||
global args
|
||||
global dbh
|
||||
@@ -152,32 +162,39 @@ def main(argv):
|
||||
access_token = None
|
||||
cookies = None
|
||||
|
||||
parser = argparse.ArgumentParser(description = 'Zwift Profile Fetcher')
|
||||
parser = argparse.ArgumentParser(description='Zwift Profile Fetcher')
|
||||
parser.add_argument('-v', '--verbose', action='store_true',
|
||||
help='Verbose output')
|
||||
help='Verbose output')
|
||||
parser.add_argument('--dont-check-certificates', action='store_false',
|
||||
dest='verifyCert', default=True)
|
||||
dest='verifyCert', default=True)
|
||||
parser.add_argument('-u', '--user', help='Zwift user name')
|
||||
args = parser.parse_args()
|
||||
|
||||
# if args.user:
|
||||
# password = getpass.getpass("Password for %s? " % args.user)
|
||||
# else:
|
||||
# file = os.environ['HOME'] + '/.zwift_cred.json'
|
||||
# with open(file) as f:
|
||||
# try:
|
||||
# cred = json.load(f)
|
||||
# except ValueError, se:
|
||||
# sys.exit('"%s": %s' % (args.output, se))
|
||||
# f.close
|
||||
# args.user = cred['user']
|
||||
# password = cred['pass']
|
||||
# if args.user:
|
||||
# password = getpass.getpass("Password for %s? " % args.user)
|
||||
# else:
|
||||
# file = os.environ['HOME'] + '/.zwift_cred.json'
|
||||
# with open(file) as f:
|
||||
# try:
|
||||
# cred = json.load(f)
|
||||
# except ValueError, se:
|
||||
# sys.exit('"%s": %s' % (args.output, se))
|
||||
# f.close
|
||||
# args.user = cred['user']
|
||||
# password = cred['pass']
|
||||
|
||||
if args.user:
|
||||
username = args.user
|
||||
else:
|
||||
username = input("Enter Zwift login (e-mail): ")
|
||||
password = getpass.getpass("Enter password: ")
|
||||
if not sys.stdin.isatty(): # This terminal cannot support input without displaying text
|
||||
print(f'*WARNING* The current shell ({os.name}) cannot support hidden text entry.')
|
||||
print(f'Your password entry WILL BE VISIBLE.')
|
||||
print(f'If you are running a bash shell under windows, try executing this program via winpty:')
|
||||
print(f'>winpty python {argv[0]}')
|
||||
password = input("Enter password (will be shown):")
|
||||
else:
|
||||
password = getpass.getpass("Enter password: ")
|
||||
|
||||
session = requests.session()
|
||||
|
||||
@@ -188,10 +205,11 @@ def main(argv):
|
||||
|
||||
logout(session, refresh_token)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main(sys.argv)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except SystemExit as se:
|
||||
print("ERROR:",se)
|
||||
print("ERROR:", se)
|
||||
|
||||
@@ -1547,6 +1547,7 @@ def before_first_request():
|
||||
move_old_profile()
|
||||
init_database()
|
||||
db.create_all(app=app)
|
||||
db.session.commit() # in case create_all created a table
|
||||
check_columns()
|
||||
db.session.close()
|
||||
send_message_thread = threading.Thread(target=send_server_back_online_message)
|
||||
|
||||
Reference in New Issue
Block a user