handler.go
⎇
Raw
1package handler
2
3import (
4 "bytes"
5 "fmt"
6 "html/template"
7 "log"
8 "net/http"
9 "net/url"
10 "path/filepath"
11 "strconv"
12 "strings"
13
14 "github.com/gabriel-vasile/mimetype"
15 "github.com/go-chi/chi/v5"
16
17 "vidarchive"
18 "vidarchive/internal/config"
19 "vidarchive/internal/models"
20 "vidarchive/internal/service"
21 "vidarchive/internal/worker"
22)
23
24type Handler struct {
25 cfg *config.Config
26 templates *template.Template
27 presetSvc *service.PresetService
28 downloadSvc *service.DownloadService
29 librarySvc *service.LibraryService
30 settingsSvc *service.SettingsService
31 subscriptionSvc *service.SubscriptionService
32 workerPool *worker.Pool
33}
34
35func New(cfg *config.Config, presetSvc *service.PresetService, downloadSvc *service.DownloadService, librarySvc *service.LibraryService, settingsSvc *service.SettingsService, subscriptionSvc *service.SubscriptionService, workerPool *worker.Pool) (*Handler, error) {
36 tmpl, err := loadTemplates(presetSvc)
37 if err != nil {
38 return nil, fmt.Errorf("load templates: %w", err)
39 }
40
41 return &Handler{
42 cfg: cfg,
43 templates: tmpl,
44 presetSvc: presetSvc,
45 downloadSvc: downloadSvc,
46 librarySvc: librarySvc,
47 settingsSvc: settingsSvc,
48 subscriptionSvc: subscriptionSvc,
49 workerPool: workerPool,
50 }, nil
51}
52
53// loadTemplates builds the template set. The preset service is passed in and
54// captured by the presetFlags func rather than reached through a package-level
55// variable, so the handler owns its dependencies.
56func loadTemplates(presetSvc *service.PresetService) (*template.Template, error) {
57 tmpl := template.New("").Funcs(template.FuncMap{
58 "formatDuration": formatDuration,
59 "formatFileSize": formatFileSize,
60 "splitPath": func(p string) []string { return strings.Split(strings.Trim(p, "/"), "/") },
61 "baseName": filepath.Base,
62 "ext": filepath.Ext,
63 "mimeType": mimeTypeFromFile,
64 "presetFlags": func(p *models.Preset) string { return presetSvc.EffectiveFlags(p, "", "") },
65 "urlEncode": template.URLQueryEscaper,
66 "urlEncodePath": func(p string) string {
67 parts := strings.Split(strings.Trim(p, "/"), "/")
68 for i, part := range parts {
69 parts[i] = url.PathEscape(part)
70 }
71 return strings.Join(parts, "/")
72 },
73 "sub": func(a, b int) int { return a - b },
74 // emptyPreset / newSubscription supply a zero value so the shared create and
75 // edit form partials can be rendered from the create page too. newSubscription
76 // carries the create-time defaults (overwrite mode, daily schedule).
77 "emptyPreset": func() *models.Preset { return nil },
78 "newSubscription": func() *models.Subscription {
79 return &models.Subscription{RefreshMode: "overwrite", ScheduleKind: "daily"}
80 },
81 // dict builds a map from alternating key/value args, so a template can pass
82 // more than one value into a sub-template (e.g. the subscription form needs
83 // both the subscription and the preset list).
84 "dict": func(values ...interface{}) (map[string]interface{}, error) {
85 if len(values)%2 != 0 {
86 return nil, fmt.Errorf("dict expects an even number of arguments")
87 }
88 m := make(map[string]interface{}, len(values)/2)
89 for i := 0; i < len(values); i += 2 {
90 key, ok := values[i].(string)
91 if !ok {
92 return nil, fmt.Errorf("dict keys must be strings")
93 }
94 m[key] = values[i+1]
95 }
96 return m, nil
97 },
98 // isLongText reports whether text spans more than ~2 lines, so the detail
99 // view can make long descriptions collapsible. Uses rune count (not bytes)
100 // so CJK text isn't flagged early.
101 "isLongText": func(s string) bool {
102 return strings.Count(s, "\n") >= 2 || len([]rune(s)) > 180
103 },
104 })
105
106 return tmpl.ParseFS(vidarchive.TemplatesFS, "web/templates/*.html")
107}
108
109func formatDuration(seconds int) string {
110 if seconds <= 0 {
111 return "--:--"
112 }
113 m := seconds / 60
114 s := seconds % 60
115 h := m / 60
116 m = m % 60
117 if h > 0 {
118 return fmt.Sprintf("%d:%02d:%02d", h, m, s)
119 }
120 return fmt.Sprintf("%d:%02d", m, s)
121}
122
123func formatFileSize(bytes int64) string {
124 const unit = 1024
125 if bytes < unit {
126 return fmt.Sprintf("%d B", bytes)
127 }
128 div, exp := int64(unit), 0
129 for n := bytes / unit; n >= unit; n /= unit {
130 div *= unit
131 exp++
132 }
133 return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
134}
135
136func mimeTypeFromFile(path string) string {
137 mtype, err := mimetype.DetectFile(path)
138 if err != nil || mtype == nil {
139 return "application/octet-stream"
140 }
141 return mtype.String()
142}
143
144type PageData struct {
145 Title string
146 ActiveTab string
147 Data interface{}
148 BaseURL string
149 AutoRefresh bool
150 RefreshSec int
151 Theme string
152 Flash *Flash
153}
154
155// Flash is a one-shot message shown to the user after a redirect (the
156// Post/Redirect/Get pattern). Kind is "success" or "error".
157type Flash struct {
158 Kind string
159 Message string
160}
161
162const cookieMaxAge = 365 * 24 * 60 * 60
163
164const flashCookie = "flash"
165
166// setFlash stashes a one-shot message in a short-lived cookie. The next rendered
167// page reads and clears it (see consumeFlash), so the message appears once after
168// the redirect and never again.
169func setFlash(w http.ResponseWriter, kind, message string) {
170 http.SetCookie(w, &http.Cookie{
171 Name: flashCookie,
172 Value: url.QueryEscape(kind + "|" + message),
173 Path: "/",
174 MaxAge: 30,
175 HttpOnly: true,
176 SameSite: http.SameSiteLaxMode,
177 })
178}
179
180func flashSuccess(w http.ResponseWriter, message string) { setFlash(w, "success", message) }
181func flashError(w http.ResponseWriter, message string) { setFlash(w, "error", message) }
182
183// consumeFlash reads the flash cookie (if any) and immediately expires it, so a
184// message is shown exactly once.
185func consumeFlash(w http.ResponseWriter, r *http.Request) *Flash {
186 c, err := r.Cookie(flashCookie)
187 if err != nil || c.Value == "" {
188 return nil
189 }
190 http.SetCookie(w, &http.Cookie{
191 Name: flashCookie,
192 Value: "",
193 Path: "/",
194 MaxAge: -1,
195 HttpOnly: true,
196 SameSite: http.SameSiteLaxMode,
197 })
198 decoded, err := url.QueryUnescape(c.Value)
199 if err != nil {
200 return nil
201 }
202 kind, message, ok := strings.Cut(decoded, "|")
203 if !ok {
204 return nil
205 }
206 // The cookie is client-editable, so don't let an arbitrary kind flow into the
207 // banner's class name — clamp it to the two we render.
208 if kind != "success" {
209 kind = "error"
210 }
211 return &Flash{Kind: kind, Message: message}
212}
213
214func getCookie(r *http.Request, name, defaultVal string) string {
215 cookie, err := r.Cookie(name)
216 if err != nil {
217 return defaultVal
218 }
219 return cookie.Value
220}
221
222func setCookie(w http.ResponseWriter, name, value string) {
223 http.SetCookie(w, &http.Cookie{
224 Name: name,
225 Value: value,
226 Path: "/",
227 MaxAge: cookieMaxAge,
228 HttpOnly: true,
229 SameSite: http.SameSiteLaxMode,
230 })
231}
232
233func (h *Handler) renderWithRequest(w http.ResponseWriter, r *http.Request, contentTmpl string, data PageData) {
234 data.BaseURL = h.cfg.BaseURL
235 if data.Theme == "" {
236 data.Theme = getCookie(r, "theme", "auto")
237 }
238 if data.Flash == nil {
239 data.Flash = consumeFlash(w, r)
240 }
241 w.Header().Set("Content-Type", "text/html; charset=utf-8")
242
243 var contentBuf bytes.Buffer
244 if err := h.templates.ExecuteTemplate(&contentBuf, contentTmpl, data); err != nil {
245 http.Error(w, fmt.Sprintf("render content: %v", err), http.StatusInternalServerError)
246 return
247 }
248
249 layoutData := struct {
250 PageData
251 Content template.HTML
252 }{
253 PageData: data,
254 Content: template.HTML(contentBuf.String()),
255 }
256
257 if err := h.templates.ExecuteTemplate(w, "layout", layoutData); err != nil {
258 http.Error(w, fmt.Sprintf("render layout: %v", err), http.StatusInternalServerError)
259 }
260}
261
262// settingsOrDefault loads settings for page rendering, falling back to sane
263// defaults (rather than a nil deref) if the store can't be read.
264func (h *Handler) settingsOrDefault() *models.Settings {
265 settings, err := h.settingsSvc.GetAll()
266 if err != nil {
267 log.Printf("settings load failed, using defaults: %v", err)
268 return &models.Settings{RefreshInterval: 5, AutoRefreshDownloads: true}
269 }
270 return settings
271}
272
273func (h *Handler) Library(w http.ResponseWriter, r *http.Request) {
274 path := r.URL.Query().Get("path")
275 filter := r.URL.Query().Get("filter")
276 sortBy := sortFromRequest(w, r, "library_sort", "date")
277
278 items, folders, err := h.librarySvc.GetAll(path, sortBy, filter)
279 if err != nil {
280 h.serverError(w, r, "list library", err)
281 return
282 }
283
284 settings := h.settingsOrDefault()
285
286 h.renderWithRequest(w, r, "library", PageData{
287 Title: "Library",
288 ActiveTab: "library",
289 AutoRefresh: settings.AutoRefreshLibrary,
290 RefreshSec: settings.RefreshInterval,
291 Data: struct {
292 Items []*models.LibraryItem
293 Folders []string
294 Path string
295 SortBy string
296 Filter string
297 }{
298 Items: items,
299 Folders: folders,
300 Path: path,
301 SortBy: sortBy,
302 Filter: filter,
303 },
304 })
305}
306
307func normalizeRelPath(r *http.Request) string {
308 // chi gives the raw, still-encoded wildcard. Decode it as a URL path, where
309 // '+' is a literal plus (only query strings treat '+' as space) — so an item
310 // directory named "a+b" round-trips correctly.
311 relPath := chi.URLParam(r, "*")
312 relPath = strings.Trim(relPath, "/")
313 if decoded, err := url.PathUnescape(relPath); err == nil {
314 relPath = decoded
315 } else {
316 log.Printf("normalizeRelPath: undecodable path %q: %v", relPath, err)
317 }
318 return relPath
319}
320
321func (h *Handler) LibraryItem(w http.ResponseWriter, r *http.Request) {
322 relPath := normalizeRelPath(r)
323 if r.Method == "POST" && strings.HasSuffix(relPath, "/delete") {
324 relPath = strings.TrimSuffix(relPath, "/delete")
325 h.deleteMedia(relPath, w, r)
326 return
327 }
328 h.libraryDetail(relPath, w, r)
329}
330
331func (h *Handler) libraryDetail(relPath string, w http.ResponseWriter, r *http.Request) {
332 ctx := r.Context()
333 item, err := h.librarySvc.GetByRelPath(ctx, relPath)
334 if err != nil {
335 http.Error(w, "Not found", http.StatusNotFound)
336 return
337 }
338
339 selectedFilename := r.URL.Query().Get("file")
340 if selectedFilename == "" && len(item.MediaFiles) > 0 {
341 selectedFilename = item.MediaFiles[0].Filename
342 }
343
344 meta, err := h.librarySvc.GetMetadata(ctx, relPath, selectedFilename)
345 if err != nil {
346 log.Printf("libraryDetail %q: metadata unavailable: %v", relPath, err)
347 }
348 subtitles, err := h.librarySvc.GetSubtitles(ctx, relPath)
349 if err != nil {
350 log.Printf("libraryDetail %q: subtitles unavailable: %v", relPath, err)
351 }
352
353 h.renderWithRequest(w, r, "library_detail", PageData{
354 Title: item.Name,
355 ActiveTab: "library",
356 Data: struct {
357 Item *models.LibraryItem
358 SelectedFilename string
359 Metadata *service.MediaMetadata
360 Subtitles []models.SubtitleTrack
361 }{
362 Item: item,
363 SelectedFilename: selectedFilename,
364 Metadata: meta,
365 Subtitles: subtitles,
366 },
367 })
368}
369
370func (h *Handler) ServeMediaItem(w http.ResponseWriter, r *http.Request) {
371 relPath := normalizeRelPath(r)
372 if strings.HasSuffix(relPath, "/thumbnail") {
373 h.serveThumbnail(strings.TrimSuffix(relPath, "/thumbnail"), w, r)
374 return
375 }
376 // Subtitle tracks are addressed as <item>/subtitles/<lang>, with the language
377 // as a trailing path segment (see LibraryService.GetSubtitles).
378 if i := strings.LastIndex(relPath, "/subtitles/"); i >= 0 {
379 item := relPath[:i]
380 lang := relPath[i+len("/subtitles/"):]
381 h.serveSubtitles(item, lang, w, r)
382 return
383 }
384 h.serveMedia(relPath, w, r)
385}
386
387func (h *Handler) serveMedia(relPath string, w http.ResponseWriter, r *http.Request) {
388 filename := r.URL.Query().Get("file")
389 if filename == "" {
390 http.Error(w, "Missing file", http.StatusBadRequest)
391 return
392 }
393
394 mediaPath, err := h.librarySvc.GetMediaFile(r.Context(), relPath, filename)
395 if err != nil {
396 http.Error(w, "Not found", http.StatusNotFound)
397 return
398 }
399
400 http.ServeFile(w, r, mediaPath)
401}
402
403func (h *Handler) serveThumbnail(relPath string, w http.ResponseWriter, r *http.Request) {
404 ctx := r.Context()
405 filename := r.URL.Query().Get("file")
406
407 if thumb, ok := h.librarySvc.ThumbnailForFile(ctx, relPath, filename); ok {
408 http.ServeFile(w, r, thumb)
409 return
410 }
411
412 // Fall back to an icon, matched to the requested file's type (or the item's
413 // primary file when no specific file was requested).
414 icon := "video-icon.svg"
415 if item, err := h.librarySvc.GetByRelPath(ctx, relPath); err == nil {
416 if isAudioFile(item, filename) {
417 icon = "audio-icon.svg"
418 }
419 }
420 data, err := vidarchive.StaticFS.ReadFile("web/static/icons/" + icon)
421 if err != nil {
422 http.Error(w, "icon not found", http.StatusInternalServerError)
423 return
424 }
425 w.Header().Set("Content-Type", "image/svg+xml")
426 w.Write(data)
427}
428
429// isAudioFile reports whether the named file (or, if unnamed, the first media
430// file) of an item is audio.
431func isAudioFile(item *models.LibraryItem, filename string) bool {
432 if filename != "" {
433 for _, mf := range item.MediaFiles {
434 if mf.Filename == filename {
435 return mf.IsAudio
436 }
437 }
438 return false
439 }
440 return len(item.MediaFiles) > 0 && item.MediaFiles[0].IsAudio
441}
442
443func (h *Handler) serveSubtitles(relPath, lang string, w http.ResponseWriter, r *http.Request) {
444 if lang == "" {
445 http.Error(w, "Missing language", http.StatusBadRequest)
446 return
447 }
448
449 if strings.Contains(lang, "/") || strings.Contains(lang, "..") || strings.Contains(lang, "\\") {
450 http.Error(w, "Invalid language", http.StatusBadRequest)
451 return
452 }
453
454 subtitlePath, err := h.librarySvc.GetSubtitlePath(relPath, lang)
455 if err != nil {
456 http.Error(w, "Not found", http.StatusNotFound)
457 return
458 }
459
460 w.Header().Set("Content-Type", "text/vtt")
461 http.ServeFile(w, r, subtitlePath)
462}
463
464func (h *Handler) deleteMedia(relPath string, w http.ResponseWriter, r *http.Request) {
465 if err := h.librarySvc.Delete(relPath); err != nil {
466 redirectWithError(w, r, "/library", "Couldn't delete this item.", err)
467 return
468 }
469
470 redirectWithSuccess(w, r, "/library", "Item deleted.")
471}
472
473func (h *Handler) Downloads(w http.ResponseWriter, r *http.Request) {
474 status := r.URL.Query().Get("status")
475 sortBy := sortFromRequest(w, r, "queue_sort", "date")
476
477 downloads, err := h.downloadSvc.GetAll(status, sortBy)
478 if err != nil {
479 h.serverError(w, r, "list downloads", err)
480 return
481 }
482
483 settings := h.settingsOrDefault()
484
485 h.renderWithRequest(w, r, "queue", PageData{
486 Title: "Queue",
487 ActiveTab: "queue",
488 AutoRefresh: settings.AutoRefreshDownloads,
489 RefreshSec: settings.RefreshInterval,
490 Data: struct {
491 Items []*models.Download
492 Status string
493 SortBy string
494 }{
495 Items: downloads,
496 Status: status,
497 SortBy: sortBy,
498 },
499 })
500}
501
502func (h *Handler) DownloadDetail(w http.ResponseWriter, r *http.Request) {
503 id, ok := parseID(w, r)
504 if !ok {
505 return
506 }
507
508 download, err := h.downloadSvc.GetByID(id)
509 if err != nil {
510 http.Error(w, "Not found", http.StatusNotFound)
511 return
512 }
513
514 h.renderWithRequest(w, r, "queue_detail", PageData{
515 Title: "Queue Details",
516 ActiveTab: "queue",
517 Data: download,
518 })
519}
520
521func (h *Handler) CreateDownload(w http.ResponseWriter, r *http.Request) {
522 if err := r.ParseForm(); err != nil {
523 http.Error(w, err.Error(), http.StatusBadRequest)
524 return
525 }
526
527 url := r.FormValue("url")
528 if url == "" {
529 flashError(w, "A URL is required to start a download.")
530 http.Redirect(w, r, "/download", http.StatusSeeOther)
531 return
532 }
533
534 var presetID *int64
535 if pid := r.FormValue("preset_id"); pid != "" {
536 id, err := strconv.ParseInt(pid, 10, 64)
537 if err == nil {
538 presetID = &id
539 }
540 }
541
542 formatOverride := r.FormValue("format_override")
543 customFlags := r.FormValue("custom_flags")
544 outputDir := r.FormValue("output_dir")
545
546 download, err := h.downloadSvc.Create(url, presetID, formatOverride, customFlags, outputDir)
547 if err != nil {
548 redirectWithError(w, r, "/download", "Couldn't queue this download.", err)
549 return
550 }
551
552 h.workerPool.Submit(download)
553
554 redirectWithSuccess(w, r, "/queue", "Download queued.")
555}
556
557func (h *Handler) DeleteDownload(w http.ResponseWriter, r *http.Request) {
558 id, ok := parseID(w, r)
559 if !ok {
560 return
561 }
562
563 if err := h.downloadSvc.Delete(id); err != nil {
564 redirectWithError(w, r, "/queue", "Couldn't remove this download.", err)
565 return
566 }
567
568 redirectWithSuccess(w, r, "/queue", "Download removed.")
569}
570
571func (h *Handler) ClearAllDownloads(w http.ResponseWriter, r *http.Request) {
572 if err := h.downloadSvc.DeleteAll(); err != nil {
573 redirectWithError(w, r, "/queue", "Couldn't clear the queue.", err)
574 return
575 }
576
577 redirectWithSuccess(w, r, "/queue", "Queue cleared.")
578}
579
580func (h *Handler) Settings(w http.ResponseWriter, r *http.Request) {
581 presets, err := h.presetSvc.GetAll()
582 if err != nil {
583 h.serverError(w, r, "list presets", err)
584 return
585 }
586
587 settings, err := h.settingsSvc.GetAll()
588 if err != nil {
589 h.serverError(w, r, "load settings", err)
590 return
591 }
592
593 h.renderWithRequest(w, r, "settings", PageData{
594 Title: "Settings",
595 ActiveTab: "settings",
596 Data: struct {
597 Presets []*models.Preset
598 Settings *models.Settings
599 }{
600 Presets: presets,
601 Settings: settings,
602 },
603 })
604}
605
606func (h *Handler) CreatePreset(w http.ResponseWriter, r *http.Request) {
607 if err := r.ParseForm(); err != nil {
608 http.Error(w, err.Error(), http.StatusBadRequest)
609 return
610 }
611
612 preset := &models.Preset{}
613 if err := applyPresetForm(preset, r); err != nil {
614 redirectWithError(w, r, "/settings", err.Error(), nil)
615 return
616 }
617
618 if err := h.presetSvc.Create(preset); err != nil {
619 redirectWithError(w, r, "/settings", "Couldn't create this preset.", err)
620 return
621 }
622
623 redirectWithSuccess(w, r, "/settings", "Preset created.")
624}
625
626// applyPresetForm copies the preset form fields onto p and validates them. It is
627// shared by create and update so the two can't drift apart as fields are added.
628func applyPresetForm(p *models.Preset, r *http.Request) error {
629 name := strings.TrimSpace(r.FormValue("name"))
630 if name == "" {
631 return fmt.Errorf("A preset needs a name.")
632 }
633
634 // Mirrors the radio options on the settings form; empty means "unspecified"
635 // and BuildArgs applies its own default.
636 formatMode := r.FormValue("format_mode")
637 switch formatMode {
638 case "", "default", "preset", "custom":
639 default:
640 return fmt.Errorf("Unknown format mode %q.", formatMode)
641 }
642
643 p.Name = name
644 p.Description = r.FormValue("description")
645 p.FormatMode = formatMode
646 p.Format = r.FormValue("format")
647 p.Quality = r.FormValue("quality")
648 p.CustomFormat = r.FormValue("custom_format")
649 p.AudioFormat = r.FormValue("audio_format")
650 p.SubLangs = r.FormValue("sub_langs")
651 p.CustomFlags = r.FormValue("custom_flags")
652 p.IsDefault = r.FormValue("is_default") == "1"
653 p.ExtractAudio = r.FormValue("extract_audio") == "1"
654 p.EmbedSubs = r.FormValue("embed_subs") == "1"
655 p.EmbedThumbnail = r.FormValue("embed_thumbnail") == "1"
656 p.EmbedMetadata = r.FormValue("embed_metadata") == "1"
657 p.WriteInfoJSON = r.FormValue("write_info_json") == "1"
658
659 return nil
660}
661
662func (h *Handler) UpdatePreset(w http.ResponseWriter, r *http.Request) {
663 id, ok := parseID(w, r)
664 if !ok {
665 return
666 }
667
668 if err := r.ParseForm(); err != nil {
669 http.Error(w, err.Error(), http.StatusBadRequest)
670 return
671 }
672
673 preset, err := h.presetSvc.GetByID(id)
674 if err != nil {
675 http.Error(w, "Not found", http.StatusNotFound)
676 return
677 }
678
679 if err := applyPresetForm(preset, r); err != nil {
680 redirectWithError(w, r, "/settings", err.Error(), nil)
681 return
682 }
683
684 if err := h.presetSvc.Update(preset); err != nil {
685 redirectWithError(w, r, "/settings", "Couldn't update this preset.", err)
686 return
687 }
688
689 redirectWithSuccess(w, r, "/settings", "Preset updated.")
690}
691
692func (h *Handler) DeletePreset(w http.ResponseWriter, r *http.Request) {
693 id, ok := parseID(w, r)
694 if !ok {
695 return
696 }
697
698 if err := h.presetSvc.Delete(id); err != nil {
699 redirectWithError(w, r, "/settings", "Couldn't delete this preset.", err)
700 return
701 }
702
703 redirectWithSuccess(w, r, "/settings", "Preset deleted.")
704}
705
706func (h *Handler) UpdateSettings(w http.ResponseWriter, r *http.Request) {
707 if err := r.ParseForm(); err != nil {
708 http.Error(w, err.Error(), http.StatusBadRequest)
709 return
710 }
711
712 var firstErr error
713 record := func(err error) {
714 if err != nil && firstErr == nil {
715 firstErr = err
716 }
717 }
718
719 if interval := r.FormValue("refresh_interval"); interval != "" {
720 record(h.settingsSvc.SetRefreshInterval(interval))
721 }
722 record(h.settingsSvc.SetAutoRefreshLibrary(r.FormValue("auto_refresh_library") == "1"))
723 record(h.settingsSvc.SetAutoRefreshDownloads(r.FormValue("auto_refresh_downloads") == "1"))
724 record(h.settingsSvc.SetCookies(r.FormValue("cookies")))
725
726 if firstErr != nil {
727 log.Printf("UpdateSettings: %v", firstErr)
728 flashError(w, "Some settings couldn't be saved: "+firstErr.Error())
729 } else {
730 flashSuccess(w, "Settings saved.")
731 }
732 http.Redirect(w, r, "/settings", http.StatusSeeOther)
733}
734
735func (h *Handler) DownloadForm(w http.ResponseWriter, r *http.Request) {
736 presets, err := h.presetSvc.GetAll()
737 if err != nil {
738 h.serverError(w, r, "list presets", err)
739 return
740 }
741
742 // A missing default preset is normal (the user may not have set one); the
743 // template handles a nil DefaultPreset, so this isn't surfaced as an error.
744 defaultPreset, _ := h.presetSvc.GetDefault()
745
746 url := r.URL.Query().Get("url")
747 var formats []*models.FormatInfo
748 var flash *Flash
749 if r.URL.Query().Get("list_formats") == "1" && url != "" {
750 var err error
751 if formats, err = h.downloadSvc.ListFormats(url); err != nil {
752 log.Printf("DownloadForm: listing formats for %q failed: %v", url, err)
753 flash = &Flash{Kind: "error", Message: "Couldn't list formats: " + err.Error()}
754 }
755 }
756
757 h.renderWithRequest(w, r, "download_form", PageData{
758 Title: "Download",
759 ActiveTab: "download",
760 Flash: flash,
761 Data: struct {
762 Presets []*models.Preset
763 DefaultPreset *models.Preset
764 URL string
765 FormatOverride string
766 CustomFlags string
767 OutputDir string
768 Formats []*models.FormatInfo
769 ShowFormats bool
770 }{
771 Presets: presets,
772 DefaultPreset: defaultPreset,
773 URL: url,
774 FormatOverride: r.URL.Query().Get("format_override"),
775 CustomFlags: r.URL.Query().Get("custom_flags"),
776 OutputDir: r.URL.Query().Get("output_dir"),
777 Formats: formats,
778 ShowFormats: r.URL.Query().Get("list_formats") == "1",
779 },
780 })
781}
782
783func (h *Handler) GetPresetFlags(w http.ResponseWriter, r *http.Request) {
784 idStr := r.URL.Query().Get("id")
785 id, err := strconv.ParseInt(idStr, 10, 64)
786 if err != nil {
787 http.Error(w, "Invalid ID", http.StatusBadRequest)
788 return
789 }
790
791 preset, err := h.presetSvc.GetByID(id)
792 if err != nil {
793 http.Error(w, "Not found", http.StatusNotFound)
794 return
795 }
796
797 flags := h.presetSvc.EffectiveFlags(preset, "", "")
798 w.Header().Set("Content-Type", "text/plain")
799 w.Write([]byte(flags))
800}
801
802func (h *Handler) Theme(w http.ResponseWriter, r *http.Request) {
803 if err := r.ParseForm(); err != nil {
804 http.Error(w, err.Error(), http.StatusBadRequest)
805 return
806 }
807
808 theme := r.FormValue("theme")
809 if theme == "" {
810 theme = "auto"
811 }
812
813 setCookie(w, "theme", theme)
814
815 referer := r.Header.Get("Referer")
816 if referer == "" {
817 referer = "/"
818 }
819 http.Redirect(w, r, referer, http.StatusSeeOther)
820}
821