feat: Initial commit of FitTrack_GarminSync project

This commit is contained in:
2025-10-10 12:20:48 -07:00
parent d0e29fbeb4
commit 18f9f6fa18
229 changed files with 21035 additions and 42 deletions

View 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