mirror of
https://github.com/sstent/FitTrack_GarminSync.git
synced 2026-01-25 08:35:23 +00:00
211 lines
8.5 KiB
Python
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
|