Files
FitTrack_GarminSync/backend/src/services/activity_download_service.py

211 lines
8.5 KiB
Python

import logging
import tempfile
import zipfile
from pathlib import Path
from typing import List, Optional
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(
"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__})"
)
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(
"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: "
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__})"
)
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 "
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:
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} "
f"(type={type(e).__name__})"
)
downloaded_path = None
return downloaded_path