package handler import ( "database/sql" "net/http" "strconv" "strings" "time" "github.com/go-chi/chi/v5" "vidarchive/internal/models" ) func (h *Handler) Subscriptions(w http.ResponseWriter, r *http.Request) { subscriptions, err := h.subscriptionSvc.GetAll() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } presets, err := h.presetSvc.GetAll() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } h.renderWithRequest(w, r, "subscriptions", PageData{ Title: "Subscriptions", ActiveTab: "subscriptions", Data: struct { Subscriptions []*models.Subscription Presets []*models.Preset }{ Subscriptions: subscriptions, Presets: presets, }, }) } // subscriptionFromForm builds and validates a Subscription from form values, // shared by create and update. It resolves/validates the schedule into a canonical // cron expression and requires an output directory (the subscription owns it). func (h *Handler) subscriptionFromForm(r *http.Request) (*models.Subscription, error) { name := strings.TrimSpace(r.FormValue("name")) url := strings.TrimSpace(r.FormValue("url")) if name == "" { return nil, errBadRequest("a name is required") } if url == "" { return nil, errBadRequest("URL is required") } outputDir := strings.TrimSpace(r.FormValue("output_dir")) if outputDir == "" { return nil, errBadRequest("an output directory is required — the subscription owns this folder") } refreshMode := r.FormValue("refresh_mode") switch refreshMode { case "overwrite", "skip", "metadata": default: refreshMode = "overwrite" } scheduleKind := r.FormValue("schedule_kind") cronExpr, err := h.subscriptionSvc.CronExprFor(scheduleKind, strings.TrimSpace(r.FormValue("cron_expr"))) if err != nil { return nil, errBadRequest(err.Error()) } sub := &models.Subscription{ Name: name, URL: url, Enabled: true, RefreshMode: refreshMode, ScheduleKind: scheduleKind, CronExpr: cronExpr, FormatOverride: r.FormValue("format_override"), CustomFlags: r.FormValue("custom_flags"), OutputDir: outputDir, PruneRemoved: r.FormValue("prune_removed") == "1", } if pid := r.FormValue("preset_id"); pid != "" { if id, err := strconv.ParseInt(pid, 10, 64); err == nil { sub.PresetID = sql.NullInt64{Int64: id, Valid: true} } } return sub, nil } func (h *Handler) CreateSubscription(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } sub, err := h.subscriptionFromForm(r) if err != nil { flashError(w, err.Error()) http.Redirect(w, r, "/subscriptions", http.StatusSeeOther) return } if next, err := h.subscriptionSvc.ComputeNextRun(sub, time.Now()); err == nil { sub.NextRunAt = sql.NullTime{Time: next, Valid: true} } if err := h.subscriptionSvc.Create(sub); err != nil { flashError(w, "Couldn't create subscription: "+err.Error()) http.Redirect(w, r, "/subscriptions", http.StatusSeeOther) return } flashSuccess(w, "Subscription created.") http.Redirect(w, r, "/subscriptions", http.StatusSeeOther) } func (h *Handler) UpdateSubscription(w http.ResponseWriter, r *http.Request) { id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { http.Error(w, "Invalid ID", http.StatusBadRequest) return } if err := r.ParseForm(); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } existing, err := h.subscriptionSvc.GetByID(id) if err != nil { http.Error(w, "Not found", http.StatusNotFound) return } sub, err := h.subscriptionFromForm(r) if err != nil { flashError(w, err.Error()) http.Redirect(w, r, "/subscriptions", http.StatusSeeOther) return } sub.ID = id sub.Enabled = existing.Enabled // Recompute the next run from the (possibly changed) schedule. if next, err := h.subscriptionSvc.ComputeNextRun(sub, time.Now()); err == nil { sub.NextRunAt = sql.NullTime{Time: next, Valid: true} } if err := h.subscriptionSvc.Update(sub); err != nil { flashError(w, "Couldn't update subscription: "+err.Error()) http.Redirect(w, r, "/subscriptions", http.StatusSeeOther) return } flashSuccess(w, "Subscription updated.") http.Redirect(w, r, "/subscriptions", http.StatusSeeOther) } func (h *Handler) ToggleSubscription(w http.ResponseWriter, r *http.Request) { id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { http.Error(w, "Invalid ID", http.StatusBadRequest) return } sub, err := h.subscriptionSvc.GetByID(id) if err != nil { http.Error(w, "Not found", http.StatusNotFound) return } if err := h.subscriptionSvc.SetEnabled(id, !sub.Enabled); err != nil { flashError(w, "Couldn't change subscription: "+err.Error()) http.Redirect(w, r, "/subscriptions", http.StatusSeeOther) return } if sub.Enabled { flashSuccess(w, "Subscription paused.") } else { flashSuccess(w, "Subscription resumed.") } http.Redirect(w, r, "/subscriptions", http.StatusSeeOther) } func (h *Handler) RunSubscription(w http.ResponseWriter, r *http.Request) { id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { http.Error(w, "Invalid ID", http.StatusBadRequest) return } sub, err := h.subscriptionSvc.GetByID(id) if err != nil { http.Error(w, "Not found", http.StatusNotFound) return } download, err := h.downloadSvc.CreateForSubscription(sub) if err != nil { flashError(w, "Couldn't start subscription run: "+err.Error()) http.Redirect(w, r, "/subscriptions", http.StatusSeeOther) return } h.workerPool.Submit(download) flashSuccess(w, "Subscription run queued.") http.Redirect(w, r, "/queue", http.StatusSeeOther) } func (h *Handler) DeleteSubscription(w http.ResponseWriter, r *http.Request) { id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err != nil { http.Error(w, "Invalid ID", http.StatusBadRequest) return } if err := h.subscriptionSvc.Delete(id); err != nil { flashError(w, "Couldn't delete subscription: "+err.Error()) http.Redirect(w, r, "/subscriptions", http.StatusSeeOther) return } flashSuccess(w, "Subscription deleted.") http.Redirect(w, r, "/subscriptions", http.StatusSeeOther) } // errBadRequest is a small sentinel-style error carrying a user-facing message. type errBadRequest string func (e errBadRequest) Error() string { return string(e) }