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