diff --git a/rowers/urls.py b/rowers/urls.py index 1e4210dc..af97a0a1 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -254,6 +254,8 @@ urlpatterns = [ name='strokedatajson_v3'), re_path(r'^api/TCX/workouts/$', views.strokedata_tcx, name='strokedata_tcx'), + re_path(r'^api/FIT/workouts/$', views.strokedata_fit, + name='strokedata_fit'), re_path(r'^api/rowingdata/workouts/$', views.strokedata_rowingdata, name='strokedata_rowingdata'), re_path(r'^api/rowingdata/$', views.strokedata_rowingdata_apikey, diff --git a/rowers/views/apiviews.py b/rowers/views/apiviews.py index b84dc6f5..143c2061 100644 --- a/rowers/views/apiviews.py +++ b/rowers/views/apiviews.py @@ -20,10 +20,25 @@ from datetime import datetime as dt import rowingdata.tcxtools as tcxtools from rowingdata import TCXParser, rowingdata +from rowingdata import FITParser as FP import arrow import base64 +# create a FITParser which parses the application/octet-stream and creates a fit file +class FITParser(BaseParser): + media_type = "application/octet-stream" + + def parse(self, stream, media_type=None, parser_context=None): + try: + return stream.read() + except Exception as e: + dologging("apilog.log", "FIT Parser") + dolofging("apilog.log", e) + raise ValueError(f"Failed to read FIT file: {str(e)}") + + return stream.read() + class XMLParser(BaseParser): media_type = "application/xml" @@ -32,10 +47,10 @@ class XMLParser(BaseParser): try: s = ET.parse(stream).getroot() except ET.XMLSyntaxError: - return HttpResponse(status=400) + raise ValueError("XML Syntax Error") except Exception as e: # pragma: no cover dologging("apilog.log",e) - return HttpResponse(status=500) + raise ValueError(f"Failed to parse XML file: {str(e)}") return s # Stroke data form to test API upload @@ -530,6 +545,83 @@ def strokedata_rowingdata_apikey(request): response.status_code = 201 return response +@csrf_exempt +@api_view(["POST"]) +@permission_required('rower.is_not_freecoach', fn=get_user_by_userid, raise_exception=True) +@permission_classes([IsAuthenticated]) +@parser_classes([FITParser]) +def strokedata_fit(request): + """ + Handle a POST request to upload a binary FIT file and save it locally. + """ + if request.method != 'POST': + return HttpResponseBadRequest("Only POST requests are allowed.") + + + try: + fit_data = request.data + + # Ensure the media directory exists + media_dir = 'media' + os.makedirs(media_dir, exist_ok=True) + + # Generate a unique filename for the FIT file + fit_filename = os.path.join(media_dir, f'{uuid4().hex[:16]}.fit') + + # Save the FIT file locally + with open(fit_filename, 'wb') as fit_file: + fit_file.write(fit_data) + except Exception as e: + return JsonResponse({ + "status": "error", + "message": f"An error occurred while saving the FIT file: {str(e)}" + }, status=500) + + try: + # Parse the FIT file + row = FP(fit_filename) + + rowdata = rowingdata(df=row.df) + duration = totaltime_sec_to_string(rowdata.duration) + title = "ActiveSpeed water" + + w = Workout.objects.create(user=request.user.rower, + duration=duration, + name=title,) + + uploadoptions = { + 'secret': UPLOAD_SERVICE_SECRET, + 'user': request.user.id, + 'file': fit_filename, + 'workouttype': 'water', + 'boattype': '1x', + 'title': title, + 'rpe': 0, + 'notes': '', + 'workoutid': w.id, + 'offline': False, + } + + url = UPLOAD_SERVICE_URL + + _ = myqueue(queuehigh, + handle_request_post, + url, + uploadoptions) + + return JsonResponse( + {"status": "success", + "workout public id": encoder.encode_hex(w.id), + "workout id": w.id, + }) + except Exception as e: + dologging('apilog.log','FIT API endpoint') + dologging('apilog.log',e) + _ = myqueue(queuehigh, handle_sendemail_unrecognized, fit_filename, "fit parser") + return HttpResponse(status=500) + + + @csrf_exempt #@login_required()