mirror of
https://github.com/sstent/FitTrack_GarminSync.git
synced 2026-01-30 11:02:14 +00:00
feat: Initial commit of FitTrack_GarminSync project
This commit is contained in:
142
backend/src/services/activity_download_service.py
Normal file
142
backend/src/services/activity_download_service.py
Normal file
@@ -0,0 +1,142 @@
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List
|
||||
import hashlib
|
||||
from datetime import datetime
|
||||
|
||||
from ..config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ActivityDownloadService:
|
||||
def __init__(self, garmin_client_instance):
|
||||
self.garmin_client = garmin_client_instance
|
||||
|
||||
def download_activity_original(self, activity_id: str, force_download: bool = False) -> Optional[Path]:
|
||||
"""Download original activity file (usually FIT format).
|
||||
|
||||
Args:
|
||||
activity_id: Garmin activity ID
|
||||
force_download: If True, bypasses checks and forces a re-download.
|
||||
|
||||
Returns:
|
||||
Path to downloaded file or None if download failed
|
||||
"""
|
||||
if not self.garmin_client.is_authenticated():
|
||||
logger.error("Garmin client not authenticated.")
|
||||
return None
|
||||
|
||||
downloaded_path = None
|
||||
|
||||
try:
|
||||
# Create data directory if it doesn't exist
|
||||
settings.GARMINSYNC_DATA_DIR.mkdir(exist_ok=True)
|
||||
|
||||
file_data = None
|
||||
attempts: List[str] = []
|
||||
|
||||
# 1) Prefer native method when available
|
||||
if hasattr(self.garmin_client.client, 'download_activity_original'):
|
||||
try:
|
||||
attempts.append("self.garmin_client.client.download_activity_original(activity_id)")
|
||||
logger.debug(f"Attempting native download_activity_original for activity {activity_id}")
|
||||
file_data = self.garmin_client.client.download_activity_original(activity_id)
|
||||
except Exception as e:
|
||||
logger.debug(f"Native download_activity_original failed: {e} (type={type(e).__name__})")
|
||||
file_data = None
|
||||
|
||||
# 2) Try download_activity with 'original' format
|
||||
if file_data is None and hasattr(self.garmin_client.client, 'download_activity'):
|
||||
try:
|
||||
attempts.append("self.garmin_client.client.download_activity(activity_id, dl_fmt=self.garmin_client.client.ActivityDownloadFormat.ORIGINAL)")
|
||||
logger.debug(f"Attempting original download via download_activity(dl_fmt=self.garmin_client.client.ActivityDownloadFormat.ORIGINAL) for activity {activity_id}")
|
||||
file_data = self.garmin_client.client.download_activity(activity_id, dl_fmt=self.garmin_client.client.ActivityDownloadFormat.ORIGINAL)
|
||||
logger.debug(f"download_activity(dl_fmt='original') succeeded, got data type: {type(file_data).__name__}, length: {len(file_data) if hasattr(file_data, '__len__') else 'N/A'}")
|
||||
if file_data is not None and hasattr(file_data, '__len__') and len(file_data) > 0:
|
||||
logger.debug(f"First 100 bytes: {file_data[:100]}")
|
||||
except Exception as e:
|
||||
logger.debug(f"download_activity(dl_fmt='original') failed: {e} (type={type(e).__name__})")
|
||||
file_data = None
|
||||
|
||||
# 3) Try download_activity with positional token (older signatures)
|
||||
if file_data is None and hasattr(self.garmin_client.client, 'download_activity'):
|
||||
tokens_to_try_pos = ['ORIGINAL', 'original', 'FIT', 'fit']
|
||||
for token in tokens_to_try_pos:
|
||||
try:
|
||||
attempts.append(f"self.garmin_client.client.download_activity(activity_id, '{token}')")
|
||||
logger.debug(f"Attempting original download via download_activity(activity_id, '{token}') for activity {activity_id}")
|
||||
file_data = self.garmin_client.client.download_activity(activity_id, token)
|
||||
logger.debug(f"download_activity(activity_id, '{token}') succeeded, got data type: {type(file_data).__name__}, length: {len(file_data) if hasattr(file_data, '__len__') else 'N/A'}")
|
||||
if file_data is not None and hasattr(file_data, '__len__') and len(file_data) > 0:
|
||||
logger.debug(f"First 100 bytes: {file_data[:100]}")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug(f"download_activity(activity_id, '{token}') failed: {e} (type={type(e).__name__})")
|
||||
file_data = None
|
||||
|
||||
if file_data is None:
|
||||
logger.error(
|
||||
f"Failed to obtain original/FIT data for activity {activity_id}. "
|
||||
f"Attempts: {attempts}"
|
||||
)
|
||||
return None
|
||||
|
||||
if hasattr(file_data, 'content'):
|
||||
try:
|
||||
file_data = file_data.content
|
||||
except Exception:
|
||||
pass
|
||||
elif hasattr(file_data, 'read'):
|
||||
try:
|
||||
file_data = file_data.read()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not isinstance(file_data, (bytes, bytearray)):
|
||||
logger.error(f"Downloaded data for activity {activity_id} is not bytes (type={type(file_data).__name__}); aborting")
|
||||
logger.debug(f"Data content: {repr(file_data)[:200]}")
|
||||
return None
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
|
||||
tmp_file.write(file_data)
|
||||
tmp_path = Path(tmp_file.name)
|
||||
|
||||
extracted_path = settings.GARMINSYNC_DATA_DIR / f"activity_{activity_id}.fit"
|
||||
|
||||
if zipfile.is_zipfile(tmp_path):
|
||||
with zipfile.ZipFile(tmp_path, 'r') as zip_ref:
|
||||
fit_files = [f for f in zip_ref.namelist() if f.lower().endswith('.fit')]
|
||||
|
||||
if fit_files:
|
||||
fit_filename = fit_files[0]
|
||||
|
||||
with zip_ref.open(fit_filename) as source, open(extracted_path, 'wb') as target:
|
||||
target.write(source.read())
|
||||
|
||||
tmp_path.unlink()
|
||||
|
||||
logger.info(f"Downloaded original activity file: {extracted_path}")
|
||||
downloaded_path = extracted_path
|
||||
else:
|
||||
logger.warning("No FIT file found in downloaded archive")
|
||||
tmp_path.unlink()
|
||||
else:
|
||||
try:
|
||||
tmp_path.rename(extracted_path)
|
||||
downloaded_path = extracted_path
|
||||
except Exception as move_err:
|
||||
logger.debug(f"Rename temp FIT to destination failed ({move_err}); falling back to copy")
|
||||
with open(extracted_path, 'wb') as target, open(tmp_path, 'rb') as source:
|
||||
target.write(source.read())
|
||||
tmp_path.unlink()
|
||||
downloaded_path = extracted_path
|
||||
logger.info(f"Downloaded original activity file: {extracted_path}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to download original activity {activity_id}: {e} (type={type(e).__name__})")
|
||||
downloaded_path = None
|
||||
|
||||
return downloaded_path
|
||||
Reference in New Issue
Block a user