package server import ( "context" "errors" "maps" "net/http" "time" "github.com/cdnutter/canon/internal/providers" "github.com/cdnutter/canon/internal/store" ) type refreshResponse struct { Item store.Item `json:"item"` Changed []string `json:"changed"` // which fields the source updated Warning string `json:"warning,omitempty"` } // apiRefreshItem fetches an entry again from the service it was added from // and applies what changed. The owner's own content is kept: the "why" // note, the rank, theater production details, and artwork they chose // themselves (unless replaceCustomArt). A field the source leaves empty // keeps its current value. func (s *Server) apiRefreshItem(w http.ResponseWriter, r *http.Request) { ctx := r.Context() id, err := pathID(r) if err != nil { s.writeErr(w, err) return } var body struct { ReplaceCustomArt bool `json:"replaceCustomArt"` } if r.ContentLength != 0 { if err := decodeJSON(r, &body); err != nil { s.writeErr(w, err) return } } cur, err := s.store.ItemByID(ctx, id) if err != nil { s.writeErr(w, err) return } list, err := s.store.ListByID(ctx, cur.ListID) if err != nil { s.writeErr(w, err) return } if list.Category == "custom" { s.writeErr(w, store.ValidationError{Msg: "Custom lists are curated by hand, so there's nothing to refresh."}) return } if cur.Provider == "" { s.writeErr(w, store.ValidationError{Msg: "This entry was added by hand, so there's no source to refresh from."}) return } lctx, cancel := context.WithTimeout(ctx, 20*time.Second) res, err := s.providers.Lookup(lctx, list.Category, cur.Provider, cur.ProviderID) cancel() var nc providers.NotConfiguredError switch { case errors.As(err, &nc): s.writeErr(w, err) return case errors.Is(err, providers.ErrGone): writeError(w, http.StatusNotFound, providerLabels[cur.Provider]+" no longer has this entry, so it was left as is.") return case err != nil: s.log.Warn("refresh lookup failed", "item", cur.ID, "provider", cur.Provider, "err", err) writeError(w, http.StatusBadGateway, providerLabels[cur.Provider]+" didn't respond: "+err.Error()) return } if res.Title != "" && !providers.SameThing(cur.Title, res.Title) { writeError(w, http.StatusConflict, providerLabels[cur.Provider]+" now calls this entry “"+res.Title+"”, which doesn't look like the same thing, so it was left as is. Edit it by hand if the change is right.") return } it := cur // Details come from the source; keep only the ones the owner enters // (theater production info) and the Deezer preview link. it.Extra = map[string]string{} for _, k := range ownerExtraKeys { if v := cur.Extra[k]; v != "" { it.Extra[k] = v } } set := func(dst *string, v string) { if v != "" { *dst = v } } set(&it.Title, res.Title) set(&it.Subtitle, res.Subtitle) set(&it.Description, res.Overview) set(&it.ExternalURL, res.ExternalURL) // A theater entry's year is the production's, which the owner set. if list.Category != "theater" { set(&it.Year, res.Year) } for k, v := range res.Extra { if v != "" { it.Extra[k] = v } } var warning string keepArt := cur.CustomArt && !body.ReplaceCustomArt if res.ImageURL != "" && !keepArt && (res.ImageURL != cur.ImageSource || cur.Image == "") { name, err := s.fetchImage(ctx, res.ImageURL) if err != nil { warning = "Kept the current artwork: " + err.Error() } else { it.Image, it.ImageSource, it.CustomArt = name, res.ImageURL, false } } if it.Title != cur.Title || it.Subtitle != cur.Subtitle { s.matchDeezer(ctx, list.Category, &it) } changed := changedFields(cur, it) if len(changed) == 0 { writeJSON(w, http.StatusOK, refreshResponse{Item: cur, Changed: []string{}, Warning: warning}) return } saved, err := s.store.UpdateItem(ctx, it) if err != nil { if it.Image != cur.Image { s.releaseImage(ctx, it.Image) } s.writeErr(w, err) return } if saved.Image != cur.Image { s.releaseImage(ctx, cur.Image) } writeJSON(w, http.StatusOK, refreshResponse{Item: saved, Changed: changed, Warning: warning}) } var ownerExtraKeys = []string{"production", "venue", "cast", "deezer"} // changedFields names what differs between two versions of an item, in the // words the editor uses. func changedFields(a, b store.Item) []string { var out []string add := func(changed bool, name string) { if changed { out = append(out, name) } } add(a.Title != b.Title, "title") add(a.Subtitle != b.Subtitle, "subtitle") add(a.Year != b.Year, "year") add(a.Description != b.Description, "description") add(a.ExternalURL != b.ExternalURL, "link") add(a.Image != b.Image, "artwork") add(!maps.Equal(a.Extra, b.Extra), "details") return out }