Prefetch Kagane covers during latest polling (#51)

## Summary
- Add an optional browser-backed cover fetcher to the latest-chapter poller.
- Prefetch missing Kagane covers during the existing due-series cycle and persist them before a Reader opens the web UI.
- Keep chapter polling, cooldown stamping, and on-first-view fallback independent from cover failures.

## Behavior and safety
- Stored Kagane covers are detected before browser work, so later poll cycles do not refetch them.
- Nil cover fetchers and non-Kagane series retain the existing behavior.
- Shared Kagane image-id and content-type validation prevents challenge or non-image responses from poisoning persistent cover storage.
- The browser is wired into both the chapter and cover poller paths from the composition root.

## Verification
- `go test ./...`
- Focused latest, store, and web package tests
- Deterministic tests cover missing covers, cached covers, failed fetches, invalid content types, nil fetchers, and non-Kagane series.

Closes #45

Reviewed-on: #51
Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com>
Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
This commit was merged in pull request #51.
This commit is contained in:
2026-08-09 14:58:59 +07:00
committed by sulthan
parent 84cfd1b2c1
commit d1800d0707
5 changed files with 258 additions and 15 deletions
+2 -12
View File
@@ -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