subscription.go
⎇
Raw
1package handler
2
3import (
4 "database/sql"
5 "fmt"
6 "log/slog"
7 "net/http"
8 "strconv"
9 "strings"
10 "time"
11
12 "vidarchive/internal/models"
13)
14
15func (h *Handler) Subscriptions(w http.ResponseWriter, r *http.Request) {
16 subscriptions, err := h.subscriptionSvc.GetAll()
17 if err != nil {
18 h.serverError(w, r, "list subscriptions", err)
19 return
20 }
21 presets, err := h.presetSvc.GetAll()
22 if err != nil {
23 h.serverError(w, r, "list presets", err)
24 return
25 }
26
27 h.renderWithRequest(w, r, "subscriptions", PageData{
28 Title: "Subscriptions",
29 ActiveTab: "subscriptions",
30 Data: struct {
31 Subscriptions []*models.Subscription
32 Presets []*models.Preset
33 }{
34 Subscriptions: subscriptions,
35 Presets: presets,
36 },
37 })
38}
39
40// subscriptionFromForm builds and validates a Subscription from form values,
41// shared by create and update. It resolves/validates the schedule into a canonical
42// cron expression and requires an output directory (the subscription owns it).
43func (h *Handler) subscriptionFromForm(r *http.Request) (*models.Subscription, error) {
44 name := strings.TrimSpace(r.FormValue("name"))
45 url := strings.TrimSpace(r.FormValue("url"))
46 if name == "" {
47 return nil, fmt.Errorf("a name is required")
48 }
49 if url == "" {
50 return nil, fmt.Errorf("URL is required")
51 }
52
53 outputDir := strings.TrimSpace(r.FormValue("output_dir"))
54 if outputDir == "" {
55 return nil, fmt.Errorf("an output directory is required — the subscription owns this folder")
56 }
57
58 refreshMode := r.FormValue("refresh_mode")
59 switch refreshMode {
60 case "overwrite", "skip", "metadata":
61 default:
62 refreshMode = "overwrite"
63 }
64
65 scheduleKind := r.FormValue("schedule_kind")
66 cronExpr, err := h.subscriptionSvc.CronExprFor(scheduleKind, strings.TrimSpace(r.FormValue("cron_expr")))
67 if err != nil {
68 return nil, err
69 }
70
71 sub := &models.Subscription{
72 Name: name,
73 URL: url,
74 Enabled: true,
75 RefreshMode: refreshMode,
76 ScheduleKind: scheduleKind,
77 CronExpr: cronExpr,
78 FormatOverride: r.FormValue("format_override"),
79 CustomFlags: r.FormValue("custom_flags"),
80 OutputDir: outputDir,
81 PruneRemoved: r.FormValue("prune_removed") == "1",
82 }
83 if pid := r.FormValue("preset_id"); pid != "" {
84 if id, err := strconv.ParseInt(pid, 10, 64); err == nil {
85 sub.PresetID = sql.NullInt64{Int64: id, Valid: true}
86 }
87 }
88 return sub, nil
89}
90
91func (h *Handler) CreateSubscription(w http.ResponseWriter, r *http.Request) {
92 if !parseForm(w, r) {
93 return
94 }
95
96 sub, err := h.subscriptionFromForm(r)
97 if err != nil {
98 redirectWithError(w, r, "/subscriptions", err.Error(), nil)
99 return
100 }
101
102 if next, err := h.subscriptionSvc.ComputeNextRun(sub, time.Now()); err == nil {
103 sub.NextRunAt = sql.NullTime{Time: next, Valid: true}
104 }
105
106 if err := h.subscriptionSvc.Create(sub); err != nil {
107 redirectWithError(w, r, "/subscriptions", "Couldn't create this subscription.", err)
108 return
109 }
110 redirectWithSuccess(w, r, "/subscriptions", "Subscription created.")
111}
112
113func (h *Handler) UpdateSubscription(w http.ResponseWriter, r *http.Request) {
114 id, ok := parseID(w, r)
115 if !ok {
116 return
117 }
118 if !parseForm(w, r) {
119 return
120 }
121
122 existing, err := h.subscriptionSvc.GetByID(id)
123 if err != nil {
124 http.Error(w, "Not found", http.StatusNotFound)
125 return
126 }
127
128 sub, err := h.subscriptionFromForm(r)
129 if err != nil {
130 redirectWithError(w, r, "/subscriptions", err.Error(), nil)
131 return
132 }
133 sub.ID = id
134 sub.Enabled = existing.Enabled
135 // Recompute the next run from the (possibly changed) schedule.
136 if next, err := h.subscriptionSvc.ComputeNextRun(sub, time.Now()); err == nil {
137 sub.NextRunAt = sql.NullTime{Time: next, Valid: true}
138 }
139
140 if err := h.subscriptionSvc.Update(sub); err != nil {
141 redirectWithError(w, r, "/subscriptions", "Couldn't update this subscription.", err)
142 return
143 }
144 redirectWithSuccess(w, r, "/subscriptions", "Subscription updated.")
145}
146
147func (h *Handler) ToggleSubscription(w http.ResponseWriter, r *http.Request) {
148 id, ok := parseID(w, r)
149 if !ok {
150 return
151 }
152 sub, err := h.subscriptionSvc.GetByID(id)
153 if err != nil {
154 http.Error(w, "Not found", http.StatusNotFound)
155 return
156 }
157 if err := h.subscriptionSvc.SetEnabled(id, !sub.Enabled); err != nil {
158 redirectWithError(w, r, "/subscriptions", "Couldn't change this subscription.", err)
159 return
160 }
161 message := "Subscription resumed."
162 if sub.Enabled {
163 message = "Subscription paused."
164 }
165 redirectWithSuccess(w, r, "/subscriptions", message)
166}
167
168func (h *Handler) RunSubscription(w http.ResponseWriter, r *http.Request) {
169 id, ok := parseID(w, r)
170 if !ok {
171 return
172 }
173 sub, err := h.subscriptionSvc.GetByID(id)
174 if err != nil {
175 http.Error(w, "Not found", http.StatusNotFound)
176 return
177 }
178
179 // Refuse a second run while one is still in flight, the same guard the
180 // scheduler applies. Two runs of one subscription share an output directory,
181 // so in overwrite mode they race: one deletes the item the other just wrote.
182 active, err := h.downloadSvc.HasActiveForSubscription(id)
183 if err != nil {
184 redirectWithError(w, r, "/subscriptions", "Couldn't start this subscription run.", err)
185 return
186 }
187 if active {
188 redirectWithError(w, r, "/subscriptions", "This subscription already has a run in progress.", nil)
189 return
190 }
191
192 download, err := h.downloadSvc.CreateForSubscription(sub)
193 if err != nil {
194 redirectWithError(w, r, "/subscriptions", "Couldn't start this subscription run.", err)
195 return
196 }
197 h.workerPool.Submit(download)
198
199 // Record the manual run so the subscriptions page shows it. The schedule's
200 // next run time is deliberately left alone.
201 if err := h.subscriptionSvc.MarkManualRun(id, time.Now(), "queued"); err != nil {
202 slog.Error("failed to record manual run", "subscription_id", id, "err", err)
203 }
204
205 redirectWithSuccess(w, r, "/queue", "Subscription run queued.")
206}
207
208func (h *Handler) DeleteSubscription(w http.ResponseWriter, r *http.Request) {
209 id, ok := parseID(w, r)
210 if !ok {
211 return
212 }
213 if err := h.subscriptionSvc.Delete(id); err != nil {
214 redirectWithError(w, r, "/subscriptions", "Couldn't delete this subscription.", err)
215 return
216 }
217 redirectWithSuccess(w, r, "/subscriptions", "Subscription deleted.")
218}
219