149
internal/monitor/monitor.go
Normal file
149
internal/monitor/monitor.go
Normal file
@@ -0,0 +1,149 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user