package storage import ( "context" "fmt" "io" "log/slog" "net/http" "time" ) // DownloadAndStoreCover fetches the image at imageURL and stores it in the // store under key. Errors are logged but not returned — this is best-effort. // If the asset is already present the download is skipped. func DownloadAndStoreCover(store Store, log *slog.Logger, key, imageURL string) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() // Skip if already stored. if _, _, ok, _ := store.GetBrowseAsset(ctx, key); ok { return } req, err := http.NewRequestWithContext(ctx, http.MethodGet, imageURL, nil) if err != nil { log.Warn("cover: build request failed", "key", key, "url", imageURL, "err", err) return } req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; libnovel-scraper/1.0)") resp, err := http.DefaultClient.Do(req) if err != nil { log.Warn("cover: fetch failed", "key", key, "url", imageURL, "err", fmt.Errorf("%w", err)) return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { log.Warn("cover: non-200 response", "key", key, "url", imageURL, "status", resp.StatusCode) return } data, err := io.ReadAll(resp.Body) if err != nil { log.Warn("cover: read body failed", "key", key, "url", imageURL, "err", err) return } contentType := resp.Header.Get("Content-Type") if contentType == "" { contentType = "image/jpeg" } if err := store.SaveBrowseAsset(ctx, key, data, contentType); err != nil { log.Warn("cover: SaveBrowseAsset failed", "key", key, "err", err) return } log.Debug("cover: stored", "key", key, "bytes", len(data)) }