mirror of
https://github.com/workhardbekind/workout-challenge.git
synced 2026-07-04 09:23:32 -04:00
194 lines
No EOL
7.9 KiB
Python
194 lines
No EOL
7.9 KiB
Python
import requests
|
|
import time, datetime
|
|
|
|
from django.core.cache import cache
|
|
from django.contrib.auth import get_user_model
|
|
from django.conf import settings
|
|
from django.utils import timezone
|
|
from rest_framework.response import Response
|
|
from rest_framework import status
|
|
from django.db import IntegrityError
|
|
from workout_challenge.celery import app, is_task_already_executing
|
|
from django.db.models import Q
|
|
|
|
from workouts.models import Workout
|
|
from .api_rate_limiter import strava_api_monitor, RateLimitExceeded # Import to trigger initialization
|
|
|
|
|
|
def _seconds_until_next_interval():
|
|
current_time = time.localtime()
|
|
minutes = current_time.tm_min
|
|
seconds = current_time.tm_sec
|
|
|
|
# Calculate the next interval (15, 30, 45, or 0 of the next hour)
|
|
next_interval = (minutes // 15 + 1) * 15
|
|
if next_interval == 60: # Handle the case where it rolls over to the next hour
|
|
next_interval = 0
|
|
|
|
# Calculate the seconds until the next interval
|
|
seconds_until = (next_interval - minutes) * 60 - seconds
|
|
if next_interval == 0: # Adjust for the next hour
|
|
seconds_until += 3600
|
|
|
|
return seconds_until
|
|
|
|
|
|
@app.task(bind=True, time_limit=60 * 60 * 3, max_retries=10) # 3 hour time limit
|
|
def daily_strava_sync(self, refresh_all=False):
|
|
if is_task_already_executing('daily_strava_sync'):
|
|
return 'Task already executing. Skipping.'
|
|
|
|
CustomUser = get_user_model()
|
|
user_lst = CustomUser.objects.filter(
|
|
strava_refresh_token__isnull=False,
|
|
is_active=True
|
|
)
|
|
if refresh_all is False:
|
|
user_lst = user_lst.filter(
|
|
Q(strava_last_synced_at__lt=timezone.now() - datetime.timedelta(hours=6)) |
|
|
Q(strava_last_synced_at__isnull=True)
|
|
)
|
|
user_lst = user_lst.order_by('strava_last_synced_at', 'pk')
|
|
|
|
user_lst_names = [{'pk': i.pk, 'username': i.username, 'email': i.email} for i in user_lst]
|
|
print(f'Syncing Strava for {len(user_lst)} users: {user_lst_names}')
|
|
|
|
for user in user_lst:
|
|
try:
|
|
sync_strava(user__id=user.id)
|
|
except RateLimitExceeded as exc:
|
|
sleep_time = _seconds_until_next_interval() + 60
|
|
print(f'Strava sync rate limit exceeded - sleeping for {sleep_time // 60 } mins')
|
|
raise self.retry(exc=exc, countdown=sleep_time) # retry in next Strava 15min api period
|
|
except Exception as exc:
|
|
print(f'Strava sync failed for user {user.email} - {exc}')
|
|
|
|
print('Finished syncing Strava.')
|
|
return user_lst_names
|
|
|
|
|
|
|
|
@app.task(bind=True)
|
|
def sync_strava(self, user__id, start_datetime=None):
|
|
access_token = cache.get(f"strava_access_token_{user__id}")
|
|
CustomUser = get_user_model()
|
|
user = CustomUser.objects.get(id=user__id)
|
|
|
|
all_existing_strava_activities = set(Workout.objects.all().values_list('strava_id', flat=True))
|
|
|
|
cnt_new_strava_activities = 0
|
|
cnt_updated_strava_activities = 0
|
|
|
|
if strava_api_monitor.ok_workout_requests() is False:
|
|
raise RateLimitExceeded("No Strava Workout API requests allowed anymore to keep enough balance for user linkage")
|
|
|
|
# refresh access token if expired
|
|
if access_token is None:
|
|
refresh_token = user.strava_refresh_token
|
|
client_id = settings.STRAVA_CLIENT_ID
|
|
client_secret = settings.STRAVA_CLIENT_SECRET
|
|
|
|
response = requests.post(
|
|
url='https://www.strava.com/oauth/token',
|
|
data={
|
|
'client_id': client_id,
|
|
'client_secret': client_secret,
|
|
'grant_type': 'refresh_token',
|
|
'refresh_token': refresh_token,
|
|
}
|
|
)
|
|
strava_api_monitor.log_request(response)
|
|
response.raise_for_status()
|
|
|
|
strava_tokens = response.json()
|
|
access_token = strava_tokens.get('access_token', None)
|
|
cache.set(f"strava_access_token_{user__id}", access_token, int(strava_tokens.get('expires_in', 21600)) - 60)
|
|
|
|
|
|
# get activities
|
|
page = 1
|
|
per_page = 200
|
|
while True:
|
|
if strava_api_monitor.ok_workout_requests() is False:
|
|
raise RateLimitExceeded("No Strava Workout API requests allowed anymore to keep enough balance for user linkage")
|
|
|
|
response = requests.get(
|
|
url='https://www.strava.com/api/v3/athlete/activities',
|
|
headers={
|
|
'Authorization': f'Bearer {access_token}',
|
|
},
|
|
params={
|
|
'after': None if start_datetime is None else int(start_datetime.timestamp()),
|
|
'page': page,
|
|
'per_page': per_page,
|
|
}
|
|
)
|
|
strava_api_monitor.log_request(response)
|
|
response.raise_for_status()
|
|
activities = response.json()
|
|
|
|
for activity in activities:
|
|
activity_id = activity.get('id')
|
|
|
|
props = {
|
|
'user': user,
|
|
'strava_id': activity_id,
|
|
'sport_type': activity.get('sport_type'),
|
|
'start_datetime': datetime.datetime.fromisoformat(activity.get('start_date')),
|
|
'duration': datetime.timedelta(seconds=activity.get('moving_time')),
|
|
'distance': None if activity.get('distance') == 0 else activity.get('distance') / 1_000,
|
|
}
|
|
|
|
# if existing workout - update activity details
|
|
if activity_id in all_existing_strava_activities:
|
|
workout = Workout.objects.get(strava_id=activity_id)
|
|
for key, value in props.items():
|
|
setattr(workout, key, value)
|
|
workout.save()
|
|
cnt_updated_strava_activities += 1
|
|
|
|
# if a new workout - get activity details
|
|
else:
|
|
if strava_api_monitor.ok_workout_requests() is False:
|
|
raise RateLimitExceeded("No Strava Workout API requests allowed anymore to keep enough balance for user linkage")
|
|
|
|
response = requests.get(
|
|
url=f'https://www.strava.com/api/v3/activities/{activity_id}',
|
|
headers={
|
|
'Authorization': f'Bearer {access_token}',
|
|
},
|
|
)
|
|
strava_api_monitor.log_request(response)
|
|
response.raise_for_status()
|
|
activity_details = response.json()
|
|
|
|
avg_heart_rate = activity_details.get('average_heartrate', 0)
|
|
props['kcal'] = kcal = activity_details.get('calories', activity_details.get('kilojoules', 0) / 4.18)
|
|
props['strava_intensity_avg_watts'] = avg_watt = activity_details.get('average_watts', 0)
|
|
|
|
# estimate intensity
|
|
max_heart_rate = 180
|
|
kcal_per_ten_minute = kcal / (max(activity.get('moving_time', 60 * 30), 60) / (60 * 10))
|
|
if avg_heart_rate > max_heart_rate * 0.85 or kcal_per_ten_minute > 120 or avg_watt > 300:
|
|
props['intensity_category'] = 4
|
|
elif avg_heart_rate > max_heart_rate * 0.70 or kcal_per_ten_minute > 90 or avg_watt > 275:
|
|
props['intensity_category'] = 3
|
|
elif avg_heart_rate > max_heart_rate * 0.60 or kcal_per_ten_minute > 75 or avg_watt > 225:
|
|
props['intensity_category'] = 2
|
|
else:
|
|
props['intensity_category'] = 1
|
|
|
|
Workout.objects.create(**props)
|
|
cnt_new_strava_activities += 1
|
|
|
|
if len(activities) < per_page:
|
|
break
|
|
page += 1
|
|
|
|
strava_last_synced_at = timezone.now()
|
|
if start_datetime is None:
|
|
setattr(user, 'strava_last_synced_at', strava_last_synced_at)
|
|
user.save()
|
|
print(f'User {user__id} - fetched {cnt_new_strava_activities} new strava activities and updated {cnt_updated_strava_activities} existing strava activities')
|
|
|
|
return {'user': user__id, 'total_activities': (page - 1) * per_page + len(activities), 'new_activities': cnt_new_strava_activities, 'updated_activities': cnt_updated_strava_activities, 'sync_time': strava_last_synced_at} |