From 53e6fefbfe4b1bd4541c97209410993c9908aaf4 Mon Sep 17 00:00:00 2001
From: Sander Roosendaal
Date: Sun, 8 Dec 2024 17:44:45 +0100
Subject: [PATCH] import somewhat working (workout type not)
---
rowers/integrations/integrations.py | 2 +-
rowers/integrations/intervals.py | 102 +++++++++++++++++++--
rowers/models.py | 7 +-
rowers/tasks.py | 69 ++++++++++++++
rowers/templates/menu_workouts.html | 1 +
rowers/templates/rower_exportsettings.html | 2 +
rowers/views/importviews.py | 32 +++++++
rowsandall_app/urls.py | 1 +
static/img/intervals_icu.png | Bin 0 -> 3776 bytes
static/img/ms-icon-120x120.png | Bin 0 -> 13739 bytes
10 files changed, 202 insertions(+), 14 deletions(-)
create mode 100644 static/img/intervals_icu.png
create mode 100644 static/img/ms-icon-120x120.png
diff --git a/rowers/integrations/integrations.py b/rowers/integrations/integrations.py
index 0cfaf0ad..2ff231a2 100644
--- a/rowers/integrations/integrations.py
+++ b/rowers/integrations/integrations.py
@@ -109,7 +109,7 @@ class SyncIntegration(metaclass=ABCMeta):
if 'grant_type' in self.oauth_data:
if self.oauth_data['grant_type']:
post_data['grant_type'] = self.oauth_data['grant_type']
- if 'strava' in self.oauth_data['autorization_uri']:
+ if 'strava' in self.oauth_data['authorization_uri']:
post_data['grant_type'] = "authorization_code"
if 'json' in self.oauth_data['content_type']:
diff --git a/rowers/integrations/intervals.py b/rowers/integrations/intervals.py
index e6d7e3cd..185ef8d9 100644
--- a/rowers/integrations/intervals.py
+++ b/rowers/integrations/intervals.py
@@ -6,6 +6,7 @@ from rowers import mytypes
from rowers.rower_rules import is_workout_user, ispromember
from rowers.utils import myqueue, dologging, custom_exception_handler
+from rowers.tasks import handle_intervals_getworkout
import urllib
import gzip
@@ -26,13 +27,25 @@ queue = django_rq.get_queue('default', default_timeout=3600)
queuelow = django_rq.get_queue('low', default_timeout=3600)
queuehigh = django_rq.get_queue('high', default_timeout=3600)
+
+def seconds_to_duration(seconds):
+ hours = seconds // 3600
+ minutes = (seconds % 3600) // 60
+ remaining_seconds = seconds % 60
+
+ # Format as "H:MM:SS" or "MM:SS" if no hours
+ if hours > 0:
+ return f"{int(hours)}:{int(minutes):02}:{int(remaining_seconds):02}"
+ else:
+ return f"{int(minutes)}:{int(remaining_seconds):02}"
+
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
intervals_authorize_url = 'https://intervals.icu/oauth/authorize?'
-intervals_token_url = 'https://intervals.icu/oauth/token'
+intervals_token_url = 'https://intervals.icu/api/oauth/token'
class IntervalsIntegration(SyncIntegration):
def __init__(self, *args, **kwargs):
@@ -47,14 +60,33 @@ class IntervalsIntegration(SyncIntegration):
'expirydatename': 'intervals_exp',
'refreshtokenname': 'intervals_r',
'bearer_auth': True,
- 'base_uri': 'https://intervals.icu/api/v1/',
+ 'base_url': 'https://intervals.icu/api/v1/',
'grant_type': 'refresh_token',
'headers': headers,
- 'scope': 'ACTIVITY:WRITE'
+ 'scope': 'ACTIVITY:WRITE, LIBRARY:READ',
}
def get_token(self, code, *args, **kwargs):
- return super(IntervalsIntegration, self).get_token(code, *args, **kwargs)
+ post_data = {
+ 'client_id': str(self.oauth_data['client_id']),
+ 'client_secret': self.oauth_data['client_secret'],
+ 'code': code,
+ }
+
+ response = requests.post(
+ intervals_token_url,
+ data=post_data,
+ )
+
+ if response.status_code not in [200, 201]:
+ dologging('intervals.icu.log',response.text)
+ return [0,"Failed to get token. ",0]
+
+ token_json = response.json()
+ access_token = token_json['access_token']
+ athlete = token_json['athlete']
+
+ return [access_token, athlete, '']
def get_name(self):
return 'Intervals'
@@ -63,8 +95,8 @@ class IntervalsIntegration(SyncIntegration):
return 'intervals'
def open(self, *args, **kwargs):
- dologging('intervals.icu.log', "Getting token for user {id}".format(id=self.rower.id))
- token = super(IntervalsIntegration).open(*args, **kwargs)
+ # dologging('intervals.icu.log', "Getting token for user {id}".format(id=self.rower.id))
+ token = super(IntervalsIntegration, self).open(*args, **kwargs)
return token
def createworkoutdata(self, w, *args, **kwargs) -> str:
@@ -73,13 +105,63 @@ class IntervalsIntegration(SyncIntegration):
def workout_export(self, workout, *args, **kwargs) -> str:
return NotImplemented
- def get_workouts(workout, *args, **kwargs) -> int:
- return NotImplemented
+ def get_workout_list(self, *args, **kwargs) -> int:
+ url = self.oauth_data['base_url'] + 'athlete/0/activities?'
+ startdate = timezone.now() - timedelta(days=365)
+ enddate = timezone.now() + timedelta(days=1)
+ url += 'oldest=' + startdate.strftime('%Y-%m-%d') + '&newest=' + enddate.strftime('%Y-%m-%d')
+ headers = {
+ 'accept': '*/*',
+ 'authorization': 'Bearer ' + self.open(),
+ }
+
+ response = requests.get(url, headers=headers)
+ if response.status_code != 200:
+ dologging('intervals.icu.log', response.text)
+ return []
+
+ data = response.json()
+ known_interval_ids = get_known_ids(self.rower, 'intervalsid')
+
+ workouts = []
+
+ for item in data:
+ i = item['id']
+ r = item['type']
+ d = item['distance']
+ ttot = seconds_to_duration(item['moving_time'])
+ s = item['start_date']
+ s2 = ''
+ c = item['name']
+ if i in known_interval_ids:
+ nnn = ''
+ else:
+ nnn = 'NEW'
+
+ keys = ['id','distance','duration','starttime',
+ 'rowtype','source','name','new']
+
+ values = [i, d, ttot, s, r, s2, c, nnn]
+
+ ress = dict(zip(keys, values))
+ workouts.append(ress)
+
+ return workouts
+
def get_workout(self, id, *args, **kwargs) -> int:
- return NotImplemented
+ _ = self.open()
+ r = self.rower
- def get_workout_list(self, *args, **kwargs) -> list:
+ record = create_or_update_syncrecord(r, None, intervalsid=id)
+
+ _ = myqueue(queuehigh,
+ handle_intervals_getworkout,
+ self.rower,
+ self.rower.intervals_token,
+ id)
+
+ def get_workouts(workout, *args, **kwargs) -> list:
return NotImplemented
def make_authorization_url(self, *args, **kwargs):
diff --git a/rowers/models.py b/rowers/models.py
index 635bf3ca..2075d2cf 100644
--- a/rowers/models.py
+++ b/rowers/models.py
@@ -1242,8 +1242,7 @@ class Rower(models.Model):
intervals_token = models.CharField(
default='', max_length=200, blank=True, null=True)
- intervals_exp = models.DateTimeField(blank=True, null=True)
- intervals_r = models.CharField(default='', max_length=200, blank=True, null=True)
+ intervals_owner_id = models.CharField(default='', max_length=200,blank=True, null=True)
privacychoices = (
('visible', 'Visible'),
@@ -3696,6 +3695,7 @@ class Workout(models.Model):
uploadedtogarmin = models.BigIntegerField(default=0)
uploadedtorp3 = models.BigIntegerField(default=0)
uploadedtonk = models.BigIntegerField(default=0)
+ uploadedtointervals = models.BigIntegerField(default=0)
forceunit = models.CharField(default='lbs',
choices=(
('lbs', 'lbs'),
@@ -3851,6 +3851,7 @@ class SyncRecord(models.Model):
c2id = models.BigIntegerField(unique=True,null=True,default=None)
tpid = models.BigIntegerField(unique=True,null=True,default=None)
rp3id = models.BigIntegerField(unique=True,null=True,default=None)
+ intervalsid = models.BigIntegerField(unique=True, null=True, default=None)
def save(self, *args, **kwargs):
if self.workout:
@@ -3866,7 +3867,7 @@ class SyncRecord(models.Model):
str2 = ''
- for field in ['stravaid', 'sporttracksid', 'nkid', 'c2id', 'tpid']:
+ for field in ['stravaid', 'sporttracksid', 'nkid', 'c2id', 'tpid', 'intervalsid']:
value = getattr(self, field, None)
if value is not None:
str2 += '{w}: {v},'.format(
diff --git a/rowers/tasks.py b/rowers/tasks.py
index bad2cc3c..6fc8793a 100644
--- a/rowers/tasks.py
+++ b/rowers/tasks.py
@@ -24,6 +24,7 @@ from rowers.courseutils import (
InvalidTrajectoryError
)
from rowers.emails import send_template_email
+from rowers.mytypes import fitmappinginv
from rowers.nkimportutils import (
get_nk_summary, get_nk_allstats, get_nk_intervalstats, getdict, strokeDataToDf,
add_workout_from_data
@@ -59,6 +60,8 @@ import rowingdata
from rowingdata import make_cumvalues, make_cumvalues_array
from uuid import uuid4
from rowingdata import rowingdata as rdata
+from rowingdata import FITParser as FP
+from rowingdata.otherparsers import FitSummaryData
from datetime import timedelta
@@ -3485,6 +3488,72 @@ def handle_nk_async_workout(alldata, userid, nktoken, nkid, delaysec, defaulttim
return workoutid
+@app.task
+def handle_intervals_getworkout(rower, intervalstoken, workoutid, debug=False, **kwargs):
+ authorizationstring = str('Bearer '+intervalstoken)
+ headers = {
+ 'authorization': authorizationstring,
+ }
+
+ url = "https://intervals.icu/api/v1/activity/{}".format(workoutid)
+
+ response = requests.get(url, headers=headers)
+ if response.status_code != 200:
+ return 0
+
+ data = response.json()
+ try:
+ title = data['name']
+ except KeyError:
+ title = 'Intervals workout'
+
+ try:
+ workouttype = fitmappinginv[data['type']]
+ print(data['type'])
+ except KeyError:
+ workouttype = 'water'
+
+ url = "https://intervals.icu/api/v1/activity/{workoutid}/fit-file".format(workoutid=workoutid)
+
+ response = requests.get(url, headers=headers)
+
+ if response.status_code != 200:
+ return 0
+
+ try:
+ fit_data = response.content
+ fit_filename = 'media/'+f'{uuid4().hex[:16]}.fit'
+ with open(fit_filename, 'wb') as fit_file:
+ fit_file.write(fit_data)
+ except Exception as e:
+ return 0
+
+ try:
+ row = FP(fit_filename)
+ rowdata = rowingdata.rowingdata(df=row.df)
+ rowsummary = FitSummaryData(fit_filename)
+ duration = totaltime_sec_to_string(rowdata.duration)
+ distance = rowdata.df[" Horizontal (meters)"].iloc[-1]
+ except Exception as e:
+ print(e)
+ return 0
+
+ uploadoptions = {
+ 'secret': UPLOAD_SERVICE_SECRET,
+ 'user': rower.user.id,
+ 'boattype': '1x',
+ 'workouttype': workouttype,
+ 'file': fit_filename,
+ 'title': title,
+ 'rpe': 0,
+ 'notes': '',
+ 'offline': False,
+ }
+
+ url = UPLOAD_SERVICE_URL
+ handle_request_post(url, uploadoptions)
+
+ return 1
@app.task
def handle_c2_getworkout(userid, c2token, c2id, defaulttimezone, debug=False, **kwargs):
diff --git a/rowers/templates/menu_workouts.html b/rowers/templates/menu_workouts.html
index b85f728f..2beafb52 100644
--- a/rowers/templates/menu_workouts.html
+++ b/rowers/templates/menu_workouts.html
@@ -57,6 +57,7 @@
SportTracks
Polar
RP3
+ Intervals.icu
diff --git a/rowers/templates/rower_exportsettings.html b/rowers/templates/rower_exportsettings.html
index 4990ce37..866a771a 100644
--- a/rowers/templates/rower_exportsettings.html
+++ b/rowers/templates/rower_exportsettings.html
@@ -123,6 +123,8 @@
alt="connect with RP3" width="130">

+
{% endblock %}
diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py
index f5270e17..486155b1 100644
--- a/rowers/views/importviews.py
+++ b/rowers/views/importviews.py
@@ -24,6 +24,7 @@ importauthorizeviews = {
'nk': 'rower_integration_authorize',
'rp3': 'rower_integration_authorize',
'garmin': 'rower_garmin_authorize',
+ 'intervals': 'rower_integration_authorize',
}
@@ -173,6 +174,37 @@ def rower_process_twittercallback(request): # pragma: no cover
# Process Polar Callback
+@login_required()
+def rower_process_intervalscallback(request):
+ integration = importsources['intervals'](request.user)
+ r = getrower(request.user)
+ try:
+ code = request.GET['code']
+ res = integration.get_token(code)
+ except MultiValueDictKeyError:
+ message = "The resource owner or authorization server denied the request"
+ messages.error(request, message)
+
+ url = reverse('rower_exportsettings_view')
+ return HttpResponseRedirect(url)
+
+ access_token = res[0]
+ athlete = res[1]
+ if access_token == 0:
+ message = res[1]
+ message += 'Connection to intervals.icu failed.'
+ messages.error(request, message)
+ url = reverse('rower_exportsettings_view')
+ return HttpResponseRedirect(url)
+
+ r.intervals_token = access_token
+ r.intervals_owner_id = athlete['id']
+ r.save()
+
+ successmessage = "Tokens stored. Good to go. Please check your import/export settings"
+ messages.info(request, successmessage)
+ url = reverse('rower_exportsettings_view')
+ return HttpResponseRedirect(url)
@login_required()
def rower_process_polarcallback(request):
diff --git a/rowsandall_app/urls.py b/rowsandall_app/urls.py
index caf20824..67c26a3d 100644
--- a/rowsandall_app/urls.py
+++ b/rowsandall_app/urls.py
@@ -94,6 +94,7 @@ urlpatterns += [
re_path(r'^rp3\_callback', rowersviews.rower_process_rp3callback),
re_path(r'^twitter\_callback', rowersviews.rower_process_twittercallback),
re_path(r'^idoklad\_callback', rowersviews.process_idokladcallback),
+ re_path(r'^intervals\_icu\_callback', rowersviews.rower_process_intervalscallback),
re_path(r'^i18n/', include('django.conf.urls.i18n')),
re_path(r'^tz_detect/', include('tz_detect.urls')),
re_path(r'^logo/', logoview),
diff --git a/static/img/intervals_icu.png b/static/img/intervals_icu.png
new file mode 100644
index 0000000000000000000000000000000000000000..a333c8b83e7bcfe8daba0691f4ec10014688b358
GIT binary patch
literal 3776
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