Files
Thomas Klaehn f496eebe2a Initial commit
Signed-off-by: Thomas Klaehn <thomas.klaehn@perinet.io>
2026-02-10 13:27:42 +01:00

150 lines
3.9 KiB
Go

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
}