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