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