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 http.Error(w, err.Error(), http.StatusInternalServerError)
19 return
20 }
21 presets, err := h.presetSvc.GetAll()
22 if err != nil {
23 http.Error(w, err.Error(), http.StatusInternalServerError)
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 err := r.ParseForm(); err != nil {
93 http.Error(w, err.Error(), http.StatusBadRequest)
94 return
95 }
96
97 sub, err := h.subscriptionFromForm(r)
98 if err != nil {
99 redirectWithError(w, r, "/subscriptions", err.Error(), nil)
100 return
101 }
102
103 if next, err := h.subscriptionSvc.ComputeNextRun(sub, time.Now()); err == nil {
104 sub.NextRunAt = sql.NullTime{Time: next, Valid: true}
105 }
106
107 if err := h.subscriptionSvc.Create(sub); err != nil {
108 redirectWithError(w, r, "/subscriptions", "Couldn't create this subscription.", err)
109 return
110 }
111 redirectWithSuccess(w, r, "/subscriptions", "Subscription created.")
112}
113
114func (h *Handler) UpdateSubscription(w http.ResponseWriter, r *http.Request) {
115 id, ok := parseID(w, r)
116 if !ok {
117 return
118 }
119 if err := r.ParseForm(); err != nil {
120 http.Error(w, err.Error(), http.StatusBadRequest)
121 return
122 }
123
124 existing, err := h.subscriptionSvc.GetByID(id)
125 if err != nil {
126 http.Error(w, "Not found", http.StatusNotFound)
127 return
128 }
129
130 sub, err := h.subscriptionFromForm(r)
131 if err != nil {
132 redirectWithError(w, r, "/subscriptions", err.Error(), nil)
133 return
134 }
135 sub.ID = id
136 sub.Enabled = existing.Enabled
137 // Recompute the next run from the (possibly changed) schedule.
138 if next, err := h.subscriptionSvc.ComputeNextRun(sub, time.Now()); err == nil {
139 sub.NextRunAt = sql.NullTime{Time: next, Valid: true}
140 }
141
142 if err := h.subscriptionSvc.Update(sub); err != nil {
143 redirectWithError(w, r, "/subscriptions", "Couldn't update this subscription.", err)
144 return
145 }
146 redirectWithSuccess(w, r, "/subscriptions", "Subscription updated.")
147}
148
149func (h *Handler) ToggleSubscription(w http.ResponseWriter, r *http.Request) {
150 id, ok := parseID(w, r)
151 if !ok {
152 return
153 }
154 sub, err := h.subscriptionSvc.GetByID(id)
155 if err != nil {
156 http.Error(w, "Not found", http.StatusNotFound)
157 return
158 }
159 if err := h.subscriptionSvc.SetEnabled(id, !sub.Enabled); err != nil {
160 redirectWithError(w, r, "/subscriptions", "Couldn't change this subscription.", err)
161 return
162 }
163 message := "Subscription resumed."
164 if sub.Enabled {
165 message = "Subscription paused."
166 }
167 redirectWithSuccess(w, r, "/subscriptions", message)
168}
169
170func (h *Handler) RunSubscription(w http.ResponseWriter, r *http.Request) {
171 id, ok := parseID(w, r)
172 if !ok {
173 return
174 }
175 sub, err := h.subscriptionSvc.GetByID(id)
176 if err != nil {
177 http.Error(w, "Not found", http.StatusNotFound)
178 return
179 }
180
181 // Refuse a second run while one is still in flight, the same guard the
182 // scheduler applies. Two runs of one subscription share an output directory,
183 // so in overwrite mode they race: one deletes the item the other just wrote.
184 active, err := h.downloadSvc.HasActiveForSubscription(id)
185 if err != nil {
186 redirectWithError(w, r, "/subscriptions", "Couldn't start this subscription run.", err)
187 return
188 }
189 if active {
190 redirectWithError(w, r, "/subscriptions", "This subscription already has a run in progress.", nil)
191 return
192 }
193
194 download, err := h.downloadSvc.CreateForSubscription(sub)
195 if err != nil {
196 redirectWithError(w, r, "/subscriptions", "Couldn't start this subscription run.", err)
197 return
198 }
199 h.workerPool.Submit(download)
200
201 // Record the manual run so the subscriptions page shows it. The schedule's
202 // next run time is deliberately left alone.
203 if err := h.subscriptionSvc.MarkManualRun(id, time.Now(), "queued"); err != nil {
204 slog.Error("failed to record manual run", "subscription_id", id, "err", err)
205 }
206
207 redirectWithSuccess(w, r, "/queue", "Subscription run queued.")
208}
209
210func (h *Handler) DeleteSubscription(w http.ResponseWriter, r *http.Request) {
211 id, ok := parseID(w, r)
212 if !ok {
213 return
214 }
215 if err := h.subscriptionSvc.Delete(id); err != nil {
216 redirectWithError(w, r, "/subscriptions", "Couldn't delete this subscription.", err)
217 return
218 }
219 redirectWithSuccess(w, r, "/subscriptions", "Subscription deleted.")
220}
221