package providers import ( "context" "encoding/json" "fmt" "net/http" "net/url" "strconv" "strings" "time" ) // GoogleBooks is a fallback catalogue for books. No API key for low volume. type GoogleBooks struct { Client *http.Client } // NewGoogleBooks builds the provider with a bounded HTTP client. func NewGoogleBooks() *GoogleBooks { return &GoogleBooks{Client: &http.Client{Timeout: 10 * time.Second}} } func (p *GoogleBooks) Name() string { return "googlebooks" } func (p *GoogleBooks) Kinds() []string { return []string{"ebooks"} } type gbResponse struct { Items []struct { ID string `json:"id"` VolumeInfo struct { Title string `json:"title"` Authors []string `json:"authors"` Publisher string `json:"publisher"` Language string `json:"language"` Description string `json:"description"` PublishedDate string `json:"publishedDate"` ImageLinks struct { Thumbnail string `json:"thumbnail"` } `json:"imageLinks"` IndustryIdentifiers []struct { Type string `json:"type"` Identifier string `json:"identifier"` } `json:"industryIdentifiers"` } `json:"volumeInfo"` } `json:"items"` } func (p *GoogleBooks) Search(ctx context.Context, q Query) ([]Candidate, error) { var terms []string if q.ISBN != "" { terms = append(terms, "isbn:"+q.ISBN) } else { if q.Title != "" { terms = append(terms, "intitle:"+q.Title) } if q.Author != "" { terms = append(terms, "inauthor:"+q.Author) } } if len(terms) == 0 { return nil, nil } params := url.Values{} params.Set("q", strings.Join(terms, "+")) params.Set("maxResults", "5") params.Set("printType", "books") endpoint := "https://www.googleapis.com/books/v1/volumes?" + params.Encode() req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, err } resp, err := p.Client.Do(req) if err != nil { return nil, fmt.Errorf("googlebooks: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("googlebooks: status %d", resp.StatusCode) } var parsed gbResponse if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil { return nil, fmt.Errorf("googlebooks: decode: %w", err) } out := make([]Candidate, 0, len(parsed.Items)) for _, item := range parsed.Items { v := item.VolumeInfo if v.Title == "" { continue } c := Candidate{ Provider: p.Name(), ExternalID: item.ID, Title: v.Title, Authors: v.Authors, Publisher: v.Publisher, Language: v.Language, Description: v.Description, Year: parseYear(v.PublishedDate), } // Google returns http:// thumbnail URLs; https avoids mixed content. if v.ImageLinks.Thumbnail != "" { c.CoverURL = strings.Replace(v.ImageLinks.Thumbnail, "http://", "https://", 1) } for _, id := range v.IndustryIdentifiers { if id.Type == "ISBN_13" || c.ISBN == "" { c.ISBN = id.Identifier } } out = append(out, c) } return out, nil } // parseYear extracts a leading 4-digit year from dates like "2021-05-04". func parseYear(date string) int { if len(date) < 4 { return 0 } n, err := strconv.Atoi(date[:4]) if err != nil { return 0 } return n }