subscription.go
⎇
Raw
1package handler
2
3import (
4 "database/sql"
5 "net/http"
6 "strconv"
7 "strings"
8 "time"
9
10 "vidarchive/internal/models"
11)
12
13func (h *Handler) Subscriptions(w http.ResponseWriter, r *http.Request) {
14 subscriptions, err := h.subscriptionSvc.GetAll()
15 if err != nil {
16 http.Error(w, err.Error(), http.StatusInternalServerError)
17 return
18 }
19 presets, err := h.presetSvc.GetAll()
20 if err != nil {
21 http.Error(w, err.Error(), http.StatusInternalServerError)
22 return
23 }
24
25 h.renderWithRequest(w, r, "subscriptions", PageData{
26 Title: "Subscriptions",
27 ActiveTab: "subscriptions",
28 Data: struct {
29 Subscriptions []*models.Subscription
30 Presets []*models.Preset
31 }{
32 Subscriptions: subscriptions,
33 Presets: presets,
34 },
35 })
36}
37
38// subscriptionFromForm builds and validates a Subscription from form values,
39// shared by create and update. It resolves/validates the schedule into a canonical
40// cron expression and requires an output directory (the subscription owns it).
41func (h *Handler) subscriptionFromForm(r *http.Request) (*models.Subscription, error) {
42 name := strings.TrimSpace(r.FormValue("name"))
43 url := strings.TrimSpace(r.FormValue("url"))
44 if name == "" {
45 return nil, errBadRequest("a name is required")
46 }
47 if url == "" {
48 return nil, errBadRequest("URL is required")
49 }
50
51 outputDir := strings.TrimSpace(r.FormValue("output_dir"))
52 if outputDir == "" {
53 return nil, errBadRequest("an output directory is required — the subscription owns this folder")
54 }
55
56 refreshMode := r.FormValue("refresh_mode")
57 switch refreshMode {
58 case "overwrite", "skip", "metadata":
59 default:
60 refreshMode = "overwrite"
61 }
62
63 scheduleKind := r.FormValue("schedule_kind")
64 cronExpr, err := h.subscriptionSvc.CronExprFor(scheduleKind, strings.TrimSpace(r.FormValue("cron_expr")))
65 if err != nil {
66 return nil, errBadRequest(err.Error())
67 }
68
69 sub := &models.Subscription{
70 Name: name,
71 URL: url,
72 Enabled: true,
73 RefreshMode: refreshMode,
74 ScheduleKind: scheduleKind,
75 CronExpr: cronExpr,
76 FormatOverride: r.FormValue("format_override"),
77 CustomFlags: r.FormValue("custom_flags"),
78 OutputDir: outputDir,
79 PruneRemoved: r.FormValue("prune_removed") == "1",
80 }
81 if pid := r.FormValue("preset_id"); pid != "" {
82 if id, err := strconv.ParseInt(pid, 10, 64); err == nil {
83 sub.PresetID = sql.NullInt64{Int64: id, Valid: true}
84 }
85 }
86 return sub, nil
87}
88
89func (h *Handler) CreateSubscription(w http.ResponseWriter, r *http.Request) {
90 if err := r.ParseForm(); err != nil {
91 http.Error(w, err.Error(), http.StatusBadRequest)
92 return
93 }
94
95 sub, err := h.subscriptionFromForm(r)
96 if err != nil {
97 redirectWithError(w, r, "/subscriptions", err.Error(), nil)
98 return
99 }
100
101 if next, err := h.subscriptionSvc.ComputeNextRun(sub, time.Now()); err == nil {
102 sub.NextRunAt = sql.NullTime{Time: next, Valid: true}
103 }
104
105 if err := h.subscriptionSvc.Create(sub); err != nil {
106 redirectWithError(w, r, "/subscriptions", "Couldn't create this subscription.", err)
107 return
108 }
109 redirectWithSuccess(w, r, "/subscriptions", "Subscription created.")
110}
111
112func (h *Handler) UpdateSubscription(w http.ResponseWriter, r *http.Request) {
113 id, ok := parseID(w, r)
114 if !ok {
115 return
116 }
117 if err := r.ParseForm(); err != nil {
118 http.Error(w, err.Error(), http.StatusBadRequest)
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 download, err := h.downloadSvc.CreateForSubscription(sub)
179 if err != nil {
180 redirectWithError(w, r, "/subscriptions", "Couldn't start this subscription run.", err)
181 return
182 }
183 h.workerPool.Submit(download)
184 redirectWithSuccess(w, r, "/queue", "Subscription run queued.")
185}
186
187func (h *Handler) DeleteSubscription(w http.ResponseWriter, r *http.Request) {
188 id, ok := parseID(w, r)
189 if !ok {
190 return
191 }
192 if err := h.subscriptionSvc.Delete(id); err != nil {
193 redirectWithError(w, r, "/subscriptions", "Couldn't delete this subscription.", err)
194 return
195 }
196 redirectWithSuccess(w, r, "/subscriptions", "Subscription deleted.")
197}
198
199// errBadRequest is a small sentinel-style error carrying a user-facing message.
200type errBadRequest string
201
202func (e errBadRequest) Error() string { return string(e) }
203