package server import ( "errors" "net/http" "strconv" "github.com/cdnutter/canon/internal/store" ) // ItemView is an item shaped for public display. type ItemView struct { ID int64 `json:"id"` Rank int `json:"rank"` Title string `json:"title"` Subtitle string `json:"subtitle"` Year string `json:"year"` Image string `json:"image"` ExternalURL string `json:"link"` Source string `json:"source"` Description string `json:"description"` Note string `json:"note"` Badge string `json:"badge"` // e.g. theater production Preview bool `json:"preview"` // has a playable clip at /preview/{id} Meta []MetaKV `json:"meta"` } type MetaKV struct { Label string `json:"label"` Value string `json:"value"` } var providerLabels = map[string]string{ "tmdb": "TMDB", "tvdb": "TheTVDB", "igdb": "IGDB", "deezer": "Deezer", "musicbrainz": "MusicBrainz", "wikipedia": "Wikipedia", } // extraFields lists the known item extras in display order. var extraFields = []struct{ Key, Label string }{ {"production", "Production"}, {"venue", "Venue"}, {"cast", "Notable cast"}, {"album", "Album"}, {"type", "Type"}, {"platforms", "Platforms"}, {"genres", "Genres"}, {"runtime", "Runtime"}, {"length", "Length"}, {"label", "Label"}, } func itemView(it store.Item, showNotes, previews bool) ItemView { v := ItemView{ ID: it.ID, Rank: it.Rank, Title: it.Title, Subtitle: it.Subtitle, Year: it.Year, ExternalURL: it.ExternalURL, Description: it.Description, Source: providerLabels[it.Provider], Badge: it.Extra["production"], Meta: []MetaKV{}, } if it.Image != "" { v.Image = "/images/" + it.Image } if showNotes { v.Note = it.Note } v.Preview = previews && deezerRef(it) != "" for _, f := range extraFields { if val := it.Extra[f.Key]; val != "" { v.Meta = append(v.Meta, MetaKV{f.Label, val}) } } return v } // listCategory is the list's category with the list's own artwork shape // applied. func listCategory(l store.List) store.Category { c, _ := store.CategoryByKey(l.Category) c.Aspect = l.Aspect() return c } type listSection struct { List store.List Category store.Category Items []ItemView More int // items not shown in the preview } type homePage struct { Sections []listSection TotalItems int TotalLists int } func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) { ctx := r.Context() _, loggedIn := s.currentUser(r) site := s.site(ctx) lists, err := s.store.Lists(ctx, false) if err != nil { s.serverError(w, r, err) return } var page homePage for _, l := range lists { if l.ItemCount == 0 { continue } items, err := s.store.Items(ctx, l.ID, 0, site.PreviewCount) if err != nil { s.serverError(w, r, err) return } sec := listSection{List: l, More: l.ItemCount - len(items)} sec.Category = listCategory(l) for _, it := range items { sec.Items = append(sec.Items, itemView(it, site.ShowNotes, site.ShowPreviews && hasPreviews(l.Category))) } page.Sections = append(page.Sections, sec) page.TotalItems += l.ItemCount page.TotalLists++ } // With nothing to show yet, send a fresh owner straight to the editor. if len(page.Sections) == 0 && loggedIn && len(lists) == 0 { http.Redirect(w, r, "/admin", http.StatusSeeOther) return } s.render(w, r, http.StatusOK, "home", "", page) } const listPageSize = 100 type listPage struct { List store.List Category store.Category Style string // style being shown: the list's, or the visitor's pick Styles []store.ListStyle // for the style switcher PageQS string // extra query for pager links, e.g. "&style=grid" Items []ItemView Hero *ItemView Page int Pages int PageNums []int } // visibleList loads the list named in the URL, treating hidden lists as // missing for visitors. It writes the error response when ok is false. func (s *Server) visibleList(w http.ResponseWriter, r *http.Request) (l store.List, ok bool) { l, err := s.store.ListBySlug(r.Context(), r.PathValue("slug")) if errors.Is(err, store.ErrNotFound) { s.notFound(w, r) return l, false } if err != nil { s.serverError(w, r, err) return l, false } if !l.Visible { if _, loggedIn := s.currentUser(r); !loggedIn { s.notFound(w, r) return l, false } } return l, true } func (s *Server) handleList(w http.ResponseWriter, r *http.Request) { ctx := r.Context() l, ok := s.visibleList(w, r) if !ok { return } site := s.site(ctx) p := listPage{List: l, Page: 1} p.Category = listCategory(l) p.Style, p.Styles = l.Layout, store.ListStyles if st, ok := store.StyleByKey(r.URL.Query().Get("style")); ok && st.Key != l.Layout { p.Style, p.PageQS = st.Key, "&style="+st.Key } p.Pages = max(1, (l.ItemCount+listPageSize-1)/listPageSize) if n, err := strconv.Atoi(r.URL.Query().Get("page")); err == nil && n > 1 { p.Page = min(n, p.Pages) } items, err := s.store.Items(ctx, l.ID, (p.Page-1)*listPageSize, listPageSize) if err != nil { s.serverError(w, r, err) return } for _, it := range items { p.Items = append(p.Items, itemView(it, site.ShowNotes, site.ShowPreviews && hasPreviews(l.Category))) } if len(p.Items) > 0 && p.Items[0].Rank == 1 && p.Items[0].Image != "" { p.Hero = &p.Items[0] } p.PageNums = pageWindow(p.Page, p.Pages) s.render(w, r, http.StatusOK, "list", l.Title, p) } // pageWindow returns page numbers to show around the current page, with 0 // standing in for an ellipsis. func pageWindow(cur, total int) []int { if total <= 1 { return nil } var out []int last := 0 for i := 1; i <= total; i++ { if i == 1 || i == total || (i >= cur-2 && i <= cur+2) { if last != 0 && i-last > 1 { out = append(out, 0) } out = append(out, i) last = i } } return out }