Files
GarminSync/garminsync/migrate_activities.py
2025-08-23 16:06:57 -07:00

132 lines
5.0 KiB
Python

#!/usr/bin/env python3
"""
Migration script to populate activity fields from FIT files or Garmin API
"""
import os
import sys
from datetime import datetime
import logging
from sqlalchemy import MetaData, Table, create_engine, text
from sqlalchemy.exc import OperationalError
from sqlalchemy.orm import sessionmaker
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Add parent directory to path to import garminsync modules
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from garminsync.database import Activity, get_session, init_db
from garminsync.garmin import GarminClient
from garminsync.activity_parser import get_activity_metrics
def migrate_activities():
"""Migrate activities to populate fields from FIT files or Garmin API"""
logger.info("Starting activity migration...")
# We assume database schema has been updated via Alembic migrations
# during container startup. Columns should already exist.
# Initialize Garmin client
try:
client = GarminClient()
logger.info("Garmin client initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize Garmin client: {e}")
# Continue with migration but without Garmin data
client = None
# Get database session
session = get_session()
try:
# Get all activities that need to be updated (those with NULL activity_type)
activities = session.query(Activity).filter(Activity.activity_type.is_(None)).all()
logger.info(f"Found {len(activities)} activities to migrate")
# If no activities found, exit early
if not activities:
logger.info("No activities found for migration")
return True
updated_count = 0
error_count = 0
for i, activity in enumerate(activities):
try:
logger.info(f"Processing activity {i+1}/{len(activities)} (ID: {activity.activity_id})")
# Use shared parser to get activity metrics
activity_details = get_activity_metrics(activity, client)
# Update activity fields if we have details
if activity_details:
logger.info(f"Successfully parsed metrics for activity {activity.activity_id}")
# Update activity fields
activity.activity_type = activity_details.get("activityType", {}).get("typeKey", "Unknown")
# Extract duration in seconds
duration = activity_details.get("summaryDTO", {}).get("duration")
if duration is not None:
activity.duration = int(float(duration))
# Extract distance in meters
distance = activity_details.get("summaryDTO", {}).get("distance")
if distance is not None:
activity.distance = float(distance)
# Extract max heart rate
max_hr = activity_details.get("summaryDTO", {}).get("maxHR")
if max_hr is not None:
activity.max_heart_rate = int(float(max_hr))
# Extract average power
avg_power = activity_details.get("summaryDTO", {}).get("avgPower")
if avg_power is not None:
activity.avg_power = float(avg_power)
# Extract calories
calories = activity_details.get("summaryDTO", {}).get("calories")
if calories is not None:
activity.calories = int(float(calories))
else:
# Set default values if we can't get details
activity.activity_type = "Unknown"
logger.warning(f"Could not retrieve metrics for activity {activity.activity_id}")
# Update last sync timestamp
activity.last_sync = datetime.now().isoformat()
session.commit()
updated_count += 1
# Log progress every 10 activities
if (i + 1) % 10 == 0:
logger.info(f"Progress: {i+1}/{len(activities)} activities processed")
except Exception as e:
logger.error(f"Error processing activity {activity.activity_id}: {e}")
session.rollback()
error_count += 1
continue
logger.info(f"Migration completed. Updated: {updated_count}, Errors: {error_count}")
return updated_count > 0 or error_count == 0 # Success if we updated any or had no errors
except Exception as e:
logger.error(f"Migration failed: {e}")
return False
finally:
session.close()
if __name__ == "__main__":
success = migrate_activities()
sys.exit(0 if success else 1)