subscription.go
⎇
Raw
1package handler
2
3import (
4 "database/sql"
5 "net/http"
6 "strconv"
7 "strings"
8 "time"
9
10 "github.com/go-chi/chi/v5"
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, errBadRequest("a name is required")
48 }
49 if url == "" {
50 return nil, errBadRequest("URL is required")
51 }
52
53 outputDir := strings.TrimSpace(r.FormValue("output_dir"))
54 if outputDir == "" {
55 return nil, errBadRequest("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, errBadRequest(err.Error())
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 flashError(w, err.Error())
100 http.Redirect(w, r, "/subscriptions", http.StatusSeeOther)
101 return
102 }
103
104 if next, err := h.subscriptionSvc.ComputeNextRun(sub, time.Now()); err == nil {
105 sub.NextRunAt = sql.NullTime{Time: next, Valid: true}
106 }
107
108 if err := h.subscriptionSvc.Create(sub); err != nil {
109 flashError(w, "Couldn't create subscription: "+err.Error())
110 http.Redirect(w, r, "/subscriptions", http.StatusSeeOther)
111 return
112 }
113 flashSuccess(w, "Subscription created.")
114 http.Redirect(w, r, "/subscriptions", http.StatusSeeOther)
115}
116
117func (h *Handler) UpdateSubscription(w http.ResponseWriter, r *http.Request) {
118 id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
119 if err != nil {
120 http.Error(w, "Invalid ID", http.StatusBadRequest)
121 return
122 }
123 if err := r.ParseForm(); err != nil {
124 http.Error(w, err.Error(), http.StatusBadRequest)
125 return
126 }
127
128 existing, err := h.subscriptionSvc.GetByID(id)
129 if err != nil {
130 http.Error(w, "Not found", http.StatusNotFound)
131 return
132 }
133
134 sub, err := h.subscriptionFromForm(r)
135 if err != nil {
136 flashError(w, err.Error())
137 http.Redirect(w, r, "/subscriptions", http.StatusSeeOther)
138 return
139 }
140 sub.ID = id
141 sub.Enabled = existing.Enabled
142 // Recompute the next run from the (possibly changed) schedule.
143 if next, err := h.subscriptionSvc.ComputeNextRun(sub, time.Now()); err == nil {
144 sub.NextRunAt = sql.NullTime{Time: next, Valid: true}
145 }
146
147 if err := h.subscriptionSvc.Update(sub); err != nil {
148 flashError(w, "Couldn't update subscription: "+err.Error())
149 http.Redirect(w, r, "/subscriptions", http.StatusSeeOther)
150 return
151 }
152 flashSuccess(w, "Subscription updated.")
153 http.Redirect(w, r, "/subscriptions", http.StatusSeeOther)
154}
155
156func (h *Handler) ToggleSubscription(w http.ResponseWriter, r *http.Request) {
157 id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
158 if err != nil {
159 http.Error(w, "Invalid ID", http.StatusBadRequest)
160 return
161 }
162 sub, err := h.subscriptionSvc.GetByID(id)
163 if err != nil {
164 http.Error(w, "Not found", http.StatusNotFound)
165 return
166 }
167 if err := h.subscriptionSvc.SetEnabled(id, !sub.Enabled); err != nil {
168 flashError(w, "Couldn't change subscription: "+err.Error())
169 http.Redirect(w, r, "/subscriptions", http.StatusSeeOther)
170 return
171 }
172 if sub.Enabled {
173 flashSuccess(w, "Subscription paused.")
174 } else {
175 flashSuccess(w, "Subscription resumed.")
176 }
177 http.Redirect(w, r, "/subscriptions", http.StatusSeeOther)
178}
179
180func (h *Handler) RunSubscription(w http.ResponseWriter, r *http.Request) {
181 id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
182 if err != nil {
183 http.Error(w, "Invalid ID", http.StatusBadRequest)
184 return
185 }
186 sub, err := h.subscriptionSvc.GetByID(id)
187 if err != nil {
188 http.Error(w, "Not found", http.StatusNotFound)
189 return
190 }
191 download, err := h.downloadSvc.CreateForSubscription(sub)
192 if err != nil {
193 flashError(w, "Couldn't start subscription run: "+err.Error())
194 http.Redirect(w, r, "/subscriptions", http.StatusSeeOther)
195 return
196 }
197 h.workerPool.Submit(download)
198 flashSuccess(w, "Subscription run queued.")
199 http.Redirect(w, r, "/queue", http.StatusSeeOther)
200}
201
202func (h *Handler) DeleteSubscription(w http.ResponseWriter, r *http.Request) {
203 id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
204 if err != nil {
205 http.Error(w, "Invalid ID", http.StatusBadRequest)
206 return
207 }
208 if err := h.subscriptionSvc.Delete(id); err != nil {
209 flashError(w, "Couldn't delete subscription: "+err.Error())
210 http.Redirect(w, r, "/subscriptions", http.StatusSeeOther)
211 return
212 }
213 flashSuccess(w, "Subscription deleted.")
214 http.Redirect(w, r, "/subscriptions", http.StatusSeeOther)
215}
216
217// errBadRequest is a small sentinel-style error carrying a user-facing message.
218type errBadRequest string
219
220func (e errBadRequest) Error() string { return string(e) }
221