subscription.go
⎇
Raw
1package service
2
3import (
4 "fmt"
5 "path/filepath"
6 "time"
7
8 "github.com/robfig/cron/v3"
9
10 "vidarchive/internal/config"
11 "vidarchive/internal/models"
12 "vidarchive/internal/repository"
13)
14
15type SubscriptionService struct {
16 repo *repository.SubscriptionRepository
17 cfg *config.Config
18}
19
20func NewSubscriptionService(repo *repository.SubscriptionRepository, cfg *config.Config) *SubscriptionService {
21 return &SubscriptionService{repo: repo, cfg: cfg}
22}
23
24// cronForKind maps a schedule kind to a standard 5-field cron expression. Preset
25// kinds use canonical expressions; "cron" passes the user's custom expression
26// through. Preset times default to 03:00 local to avoid the top-of-hour rush.
27func cronForKind(kind, custom string) (string, error) {
28 switch kind {
29 case "hourly":
30 return "0 * * * *", nil
31 case "daily":
32 return "0 3 * * *", nil
33 case "weekly":
34 return "0 3 * * 0", nil
35 case "monthly":
36 return "0 3 1 * *", nil
37 case "cron":
38 if custom == "" {
39 return "", fmt.Errorf("custom schedule requires a cron expression")
40 }
41 return custom, nil
42 default:
43 return "", fmt.Errorf("unknown schedule kind %q", kind)
44 }
45}
46
47// CronExprFor resolves and validates the effective cron expression for a kind +
48// custom expression, returning an error for unknown kinds or unparseable cron.
49func (s *SubscriptionService) CronExprFor(kind, custom string) (string, error) {
50 expr, err := cronForKind(kind, custom)
51 if err != nil {
52 return "", err
53 }
54 if _, err := cron.ParseStandard(expr); err != nil {
55 return "", fmt.Errorf("invalid cron expression %q: %w", expr, err)
56 }
57 return expr, nil
58}
59
60// ComputeNextRun returns the next scheduled run time strictly after `from`.
61func (s *SubscriptionService) ComputeNextRun(sub *models.Subscription, from time.Time) (time.Time, error) {
62 expr, err := s.CronExprFor(sub.ScheduleKind, sub.CronExpr)
63 if err != nil {
64 return time.Time{}, err
65 }
66 sched, err := cron.ParseStandard(expr)
67 if err != nil {
68 return time.Time{}, err
69 }
70 return sched.Next(from), nil
71}
72
73// ArchivePath is the per-subscription yt-dlp download-archive file used by
74// "skip" refresh mode to avoid re-downloading already-fetched entries.
75func (s *SubscriptionService) ArchivePath(id int64) string {
76 return filepath.Join(s.cfg.DataDir, "archives", fmt.Sprintf("sub-%d.txt", id))
77}
78
79func (s *SubscriptionService) GetAll() ([]*models.Subscription, error) {
80 return s.repo.GetAll()
81}
82
83func (s *SubscriptionService) GetByID(id int64) (*models.Subscription, error) {
84 return s.repo.GetByID(id)
85}
86
87func (s *SubscriptionService) GetDue(now time.Time) ([]*models.Subscription, error) {
88 return s.repo.GetDue(now)
89}
90
91func (s *SubscriptionService) Create(sub *models.Subscription) error {
92 return s.repo.Create(sub)
93}
94
95func (s *SubscriptionService) Update(sub *models.Subscription) error {
96 return s.repo.Update(sub)
97}
98
99func (s *SubscriptionService) SetEnabled(id int64, enabled bool) error {
100 return s.repo.SetEnabled(id, enabled)
101}
102
103func (s *SubscriptionService) MarkRun(id int64, lastRunAt, nextRunAt time.Time, status string) error {
104 return s.repo.MarkRun(id, lastRunAt, nextRunAt, status)
105}
106
107func (s *SubscriptionService) Delete(id int64) error {
108 return s.repo.Delete(id)
109}
110