161 lines
3.7 KiB
Go
161 lines
3.7 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"zwift-activity-loader/internal/client"
|
|
"zwift-activity-loader/internal/config"
|
|
"zwift-activity-loader/internal/monitor"
|
|
"zwift-activity-loader/internal/state"
|
|
"zwift-activity-loader/internal/storage"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// Service represents the main monitoring service
|
|
type Service struct {
|
|
config *config.Config
|
|
client *client.Client
|
|
monitor *monitor.Monitor
|
|
state *state.State
|
|
storage *storage.Storage
|
|
logger *zap.Logger
|
|
ticker *time.Ticker
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
// New creates a new Service instance
|
|
func New(cfg *config.Config, logger *zap.Logger) (*Service, error) {
|
|
// Create client
|
|
zwiftClient, err := client.NewClient(cfg.Username, cfg.Password, cfg.RateLimit, cfg.MaxRetries)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create Zwift client: %w", err)
|
|
}
|
|
|
|
// Create storage
|
|
stor, err := storage.New(cfg.OutputDir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create storage: %w", err)
|
|
}
|
|
|
|
// Create state
|
|
st := state.New(cfg.StateFile)
|
|
if err := st.Load(); err != nil {
|
|
return nil, fmt.Errorf("failed to load state: %w", err)
|
|
}
|
|
|
|
// Create monitor
|
|
mon := monitor.New(zwiftClient, st, stor, logger)
|
|
|
|
// Create context
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
return &Service{
|
|
config: cfg,
|
|
client: zwiftClient,
|
|
monitor: mon,
|
|
state: st,
|
|
storage: stor,
|
|
logger: logger,
|
|
ticker: time.NewTicker(cfg.PollInterval),
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}, nil
|
|
}
|
|
|
|
// Run starts the service loop
|
|
func (s *Service) Run() error {
|
|
s.logger.Info("Starting Zwift Activity Monitor",
|
|
zap.Duration("poll_interval", s.config.PollInterval),
|
|
zap.String("output_dir", s.config.OutputDir))
|
|
|
|
// Get initial stats
|
|
totalDownloaded, lastCheck := s.state.GetStats()
|
|
s.logger.Info("Loaded state",
|
|
zap.Int("total_downloaded", totalDownloaded),
|
|
zap.Time("last_check", lastCheck))
|
|
|
|
// Run initial check immediately
|
|
s.logger.Info("Running initial check...")
|
|
if err := s.runCheck(); err != nil {
|
|
s.logger.Error("Initial check failed", zap.Error(err))
|
|
}
|
|
|
|
// Start ticker loop
|
|
for {
|
|
select {
|
|
case <-s.ctx.Done():
|
|
s.logger.Info("Service context cancelled, shutting down")
|
|
return nil
|
|
|
|
case <-s.ticker.C:
|
|
if err := s.runCheck(); err != nil {
|
|
s.logger.Error("Check failed", zap.Error(err))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// runCheck runs a single check for new activities
|
|
func (s *Service) runCheck() error {
|
|
// Recover from panics
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
s.logger.Error("Panic recovered in runCheck",
|
|
zap.Any("panic", r),
|
|
zap.Stack("stack"))
|
|
}
|
|
}()
|
|
|
|
// Create context with timeout
|
|
ctx, cancel := context.WithTimeout(s.ctx, 5*time.Minute)
|
|
defer cancel()
|
|
|
|
// Run check and download
|
|
return s.monitor.CheckAndDownload(ctx)
|
|
}
|
|
|
|
// Shutdown gracefully shuts down the service
|
|
func (s *Service) Shutdown() error {
|
|
s.logger.Info("Shutting down gracefully...")
|
|
|
|
// Stop ticker
|
|
s.ticker.Stop()
|
|
|
|
// Cancel context (with timeout for current operation)
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
// Wait for current operation to complete
|
|
done := make(chan struct{})
|
|
go func() {
|
|
s.cancel()
|
|
close(done)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
s.logger.Info("Service stopped")
|
|
case <-shutdownCtx.Done():
|
|
s.logger.Warn("Shutdown timeout, forcing exit")
|
|
}
|
|
|
|
// Save final state
|
|
if err := s.state.Save(); err != nil {
|
|
s.logger.Error("Failed to save state on shutdown", zap.Error(err))
|
|
return err
|
|
}
|
|
|
|
// Close client
|
|
if err := s.client.Close(); err != nil {
|
|
s.logger.Error("Failed to close client", zap.Error(err))
|
|
return err
|
|
}
|
|
|
|
s.logger.Info("Shutdown complete")
|
|
return nil
|
|
}
|