package server import ( "context" "errors" "io" "net/http" "regexp" "strconv" "sync" "time" "github.com/cdnutter/canon/internal/providers" "github.com/cdnutter/canon/internal/store" ) // Previews are 30-second clips from Deezer, streamed through Canon so // visitors never connect to Deezer directly. Deezer's clip URLs expire, so // they're looked up on demand and cached briefly. var deezerRefRe = regexp.MustCompile(`^(track|album):[0-9]+$`) // deezerRef is the Deezer track/album an item can preview, or "". func deezerRef(it store.Item) string { ref := it.Extra["deezer"] if it.Provider == "deezer" { ref = it.ProviderID } if !deezerRefRe.MatchString(ref) { return "" } return ref } func hasPreviews(category string) bool { return category == "song" || category == "album" } type previewCache struct { mu sync.Mutex m map[string]cachedPreview } type cachedPreview struct { url string expires time.Time } func (c *previewCache) get(ref string) (string, bool) { c.mu.Lock() defer c.mu.Unlock() p, ok := c.m[ref] if !ok || time.Now().After(p.expires) { return "", false } return p.url, true } func (c *previewCache) put(ref, url string) { c.mu.Lock() defer c.mu.Unlock() if c.m == nil || len(c.m) > 2000 { c.m = map[string]cachedPreview{} } c.m[ref] = cachedPreview{url: url, expires: time.Now().Add(10 * time.Minute)} } func (c *previewCache) drop(ref string) { c.mu.Lock() defer c.mu.Unlock() delete(c.m, ref) } // handleItemPreview plays the preview for a list item. It follows the // list's visibility, like the list page does. func (s *Server) handleItemPreview(w http.ResponseWriter, r *http.Request) { id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) if err != nil { http.NotFound(w, r) return } it, err := s.store.ItemByID(r.Context(), id) if err != nil { http.NotFound(w, r) return } l, err := s.store.ListByID(r.Context(), it.ListID) if err != nil || !hasPreviews(l.Category) || !s.site(r.Context()).ShowPreviews { http.NotFound(w, r) return } if !l.Visible { if _, ok := s.currentUser(r); !ok { http.NotFound(w, r) return } } ref := deezerRef(it) if ref == "" { http.NotFound(w, r) return } s.streamPreview(w, r, ref) } // handleAdminPreview plays a search result before it's added. It's a plain // GET behind the session (an