package monitor import ( "context" "fmt" "time" "zwift-activity-loader/internal/client" "zwift-activity-loader/internal/state" "zwift-activity-loader/internal/storage" "github.com/bzimmer/activity/zwift" "go.uber.org/zap" ) // Monitor handles activity monitoring and downloading type Monitor struct { client *client.Client state *state.State storage *storage.Storage logger *zap.Logger athleteID int64 // Cached athlete ID } // New creates a new Monitor instance func New(client *client.Client, state *state.State, storage *storage.Storage, logger *zap.Logger) *Monitor { return &Monitor{ client: client, state: state, storage: storage, logger: logger, } } // CheckAndDownload checks for new activities and downloads them func (m *Monitor) CheckAndDownload(ctx context.Context) error { m.logger.Info("Checking for new activities...") // Get athlete ID (cached after first call) if m.athleteID == 0 { athleteID, err := m.client.GetProfile(ctx) if err != nil { return fmt.Errorf("failed to get profile: %w", err) } m.athleteID = athleteID m.logger.Debug("Got athlete profile", zap.Int64("athlete_id", athleteID)) } // List recent activities (last 100) activities, err := m.client.ListRecentActivities(ctx, m.athleteID, 100) if err != nil { return fmt.Errorf("failed to list activities: %w", err) } m.logger.Debug("Listed recent activities", zap.Int("count", len(activities))) // Filter new activities newActivities := m.filterNewActivities(activities) if len(newActivities) == 0 { m.logger.Info("No new activities found") if err := m.state.UpdateLastCheck(); err != nil { m.logger.Warn("Failed to update last check time", zap.Error(err)) } return nil } m.logger.Info("Found new activities", zap.Int("count", len(newActivities))) // Download new activities downloaded := 0 for _, act := range newActivities { if err := m.downloadActivity(ctx, act); err != nil { m.logger.Error("Failed to download activity", zap.Int64("activity_id", act.ID), zap.String("name", act.Name), zap.Error(err)) continue } downloaded++ } m.logger.Info("Download complete", zap.Int("downloaded", downloaded), zap.Int("total_new", len(newActivities))) return nil } // filterNewActivities filters activities that haven't been downloaded yet func (m *Monitor) filterNewActivities(activities []*zwift.Activity) []*zwift.Activity { var newActivities []*zwift.Activity for _, act := range activities { if !m.state.IsDownloaded(act.ID) { newActivities = append(newActivities, act) } } return newActivities } // downloadActivity downloads a single activity func (m *Monitor) downloadActivity(ctx context.Context, act *zwift.Activity) error { m.logger.Info("Downloading activity", zap.Int64("activity_id", act.ID), zap.String("name", act.Name), zap.Time("date", act.StartDate.Time)) // Download FIT file data, filename, err := m.client.DownloadFIT(ctx, m.athleteID, act.ID) if err != nil { return fmt.Errorf("failed to download FIT file: %w", err) } m.logger.Debug("Downloaded FIT file", zap.Int64("activity_id", act.ID), zap.String("filename", filename), zap.Int("size_bytes", len(data))) // Save to storage filepath, err := m.storage.Save(act.ID, act.Name, act.StartDate.Time, data) if err != nil { return fmt.Errorf("failed to save FIT file: %w", err) } // Mark as downloaded in state info := state.ActivityInfo{ ID: act.ID, Name: act.Name, Date: act.StartDate.Time, FilePath: filepath, DownloadedAt: time.Now(), SportType: act.Sport, } if err := m.state.MarkDownloaded(info); err != nil { m.logger.Error("Failed to mark activity as downloaded", zap.Int64("activity_id", act.ID), zap.Error(err)) // Don't return error here - file is saved, just state update failed } m.logger.Info("Successfully downloaded activity", zap.Int64("activity_id", act.ID), zap.String("name", act.Name), zap.String("filepath", filepath)) return nil }