feat: Implement Garmin sync, login improvements, and utility scripts

This commit is contained in:
2025-10-11 11:56:25 -07:00
parent 56a93cd8df
commit 3819e4f5e2
921 changed files with 2058 additions and 371 deletions

View File

@@ -1,11 +1,8 @@
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 typing import List, Optional
from ..config import settings
@@ -17,73 +14,105 @@ class ActivityDownloadService:
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}")
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__})")
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'}")
attempts.append(
"self.garmin_client.client.download_activity(activity_id, "
"dl_fmt=self.garmin_client.client.ActivityDownloadFormat.ORIGINAL)"
)
logger.debug(
"Attempting original download via download_activity("
f"dl_fmt=self.garmin_client.client.ActivityDownloadFormat.ORIGINAL) "
f"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: "
f"{type(file_data).__name__}, length: "
f"{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__})")
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}")
attempts.append(
f"self.garmin_client.client.download_activity(activity_id, '{token}')"
)
logger.debug(
"Attempting original download via download_activity("
f"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'}")
logger.debug(
f"download_activity(activity_id, '{token}') succeeded, got data type: "
f"{type(file_data).__name__}, length: "
f"{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__})")
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
@@ -94,30 +123,33 @@ class ActivityDownloadService:
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.error(
f"Downloaded data for activity {activity_id} is not bytes "
f"(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:
@@ -128,15 +160,21 @@ class ActivityDownloadService:
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")
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__})")
logger.error(
f"Failed to download original activity {activity_id}: {e} "
f"(type={type(e).__name__})"
)
downloaded_path = None
return downloaded_path
return downloaded_path