package service import ( "fmt" "path/filepath" "time" "github.com/robfig/cron/v3" "vidarchive/internal/config" "vidarchive/internal/models" "vidarchive/internal/repository" ) type SubscriptionService struct { repo *repository.SubscriptionRepository cfg *config.Config } func NewSubscriptionService(repo *repository.SubscriptionRepository, cfg *config.Config) *SubscriptionService { return &SubscriptionService{repo: repo, cfg: cfg} } // cronForKind maps a schedule kind to a standard 5-field cron expression. Preset // kinds use canonical expressions; "cron" passes the user's custom expression // through. Preset times default to 03:00 local to avoid the top-of-hour rush. func cronForKind(kind, custom string) (string, error) { switch kind { case "hourly": return "0 * * * *", nil case "daily": return "0 3 * * *", nil case "weekly": return "0 3 * * 0", nil case "monthly": return "0 3 1 * *", nil case "cron": if custom == "" { return "", fmt.Errorf("custom schedule requires a cron expression") } return custom, nil default: return "", fmt.Errorf("unknown schedule kind %q", kind) } } // CronExprFor resolves and validates the effective cron expression for a kind + // custom expression, returning an error for unknown kinds or unparseable cron. func (s *SubscriptionService) CronExprFor(kind, custom string) (string, error) { expr, err := cronForKind(kind, custom) if err != nil { return "", err } if _, err := cron.ParseStandard(expr); err != nil { return "", fmt.Errorf("invalid cron expression %q: %w", expr, err) } return expr, nil } // ComputeNextRun returns the next scheduled run time strictly after `from`. func (s *SubscriptionService) ComputeNextRun(sub *models.Subscription, from time.Time) (time.Time, error) { expr, err := s.CronExprFor(sub.ScheduleKind, sub.CronExpr) if err != nil { return time.Time{}, err } sched, err := cron.ParseStandard(expr) if err != nil { return time.Time{}, err } return sched.Next(from), nil } // ArchivePath is the per-subscription yt-dlp download-archive file used by // "skip" refresh mode to avoid re-downloading already-fetched entries. func (s *SubscriptionService) ArchivePath(id int64) string { return filepath.Join(s.cfg.DataDir, "archives", fmt.Sprintf("sub-%d.txt", id)) } func (s *SubscriptionService) GetAll() ([]*models.Subscription, error) { return s.repo.GetAll() } func (s *SubscriptionService) GetByID(id int64) (*models.Subscription, error) { return s.repo.GetByID(id) } func (s *SubscriptionService) GetDue(now time.Time) ([]*models.Subscription, error) { return s.repo.GetDue(now) } func (s *SubscriptionService) Create(sub *models.Subscription) error { return s.repo.Create(sub) } func (s *SubscriptionService) Update(sub *models.Subscription) error { return s.repo.Update(sub) } func (s *SubscriptionService) SetEnabled(id int64, enabled bool) error { return s.repo.SetEnabled(id, enabled) } func (s *SubscriptionService) MarkRun(id int64, lastRunAt, nextRunAt time.Time, status string) error { return s.repo.MarkRun(id, lastRunAt, nextRunAt, status) } func (s *SubscriptionService) Delete(id int64) error { return s.repo.Delete(id) }