feat(latest): prefetch kagane covers during polling
Add an optional cover fetcher to the latest-chapter poller and wire the shared browser into it. Each due Kagane series now checks persistent storage, fetches a missing cover once, and keeps chapter polling and cooldown bookkeeping independent when cover work fails.\n\nCentralize Kagane image-id and content-type validation so prefetched bytes cannot poison the on-first-view fallback, and cover the missing, cached, failure, nil-fetcher, invalid-type, and non-Kagane paths with deterministic poll-cycle tests.\n\nRefs #45
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"bookmarkmanager/backend/internal/store"
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -21,17 +22,6 @@ type CoverFetcher interface {
|
||||
// so it is validated at the boundary rather than passed through.
|
||||
var coverIDRe = regexp.MustCompile(`^[0-9a-f-]{36}$`)
|
||||
|
||||
// coverTypes is the set of content types the proxy will echo back. A response
|
||||
// header sourced from a third party is not repeated verbatim: anything outside
|
||||
// this set is treated as "not a cover".
|
||||
var coverTypes = map[string]bool{
|
||||
"image/webp": true,
|
||||
"image/jpeg": true,
|
||||
"image/png": true,
|
||||
"image/avif": true,
|
||||
"image/gif": true,
|
||||
}
|
||||
|
||||
// coverTimeout bounds one proxied cover. Shorter than the fetcher's own
|
||||
// challenge budget on purpose: a browser page is waiting on this, and a cover
|
||||
// that has not arrived by now is better left as a broken slot than as a request
|
||||
@@ -75,7 +65,7 @@ func (h *Handler) kaganeCover(w http.ResponseWriter, r *http.Request) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if !coverTypes[contentType] {
|
||||
if !store.IsKaganeCoverContentType(contentType) {
|
||||
log.Printf("kagane cover %s: unexpected content type %q", id, contentType)
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user