package service import ( "context" "encoding/json" "fmt" "net/http" "time" ) type OpenLibraryProvider struct { baseURL string httpClient *http.Client } func NewOpenLibraryProvider() *OpenLibraryProvider { return &OpenLibraryProvider{ baseURL: "https://openlibrary.org", httpClient: &http.Client{ Timeout: 10 * time.Second, }, } } type olSearchResponse struct { NumFound int `json:"numFound"` Docs []olDoc `json:"docs"` } type olDoc struct { Key string `json:"key"` Title string `json:"title"` FirstPublishYear int `json:"first_publish_year"` AuthorName []string `json:"author_name"` ISBN []string `json:"isbn"` Publisher []string `json:"publisher"` CoverID int `json:"cover_i"` Subject []string `json:"subject"` } type olWorkDetail struct { Title string `json:"title"` Description interface{} `json:"description"` Covers []int `json:"covers"` Authors []olAuthorRef `json:"authors"` } type olAuthorRef struct { Author struct { Key string `json:"key"` } `json:"author"` } type olAuthorDetail struct { Name string `json:"name"` Bio interface{} `json:"bio"` BirthDate string `json:"birth_date"` DeathDate string `json:"death_date"` Photos []int `json:"photos"` } func (p *OpenLibraryProvider) Name() string { return "openlibrary" } func (p *OpenLibraryProvider) fetch(ctx context.Context, url string, result interface{}) error { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return fmt.Errorf("create request: %w", err) } resp, err := p.httpClient.Do(req) if err != nil { return fmt.Errorf("fetch open library: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("open library api returned status %d", resp.StatusCode) } if err := json.NewDecoder(resp.Body).Decode(result); err != nil { return fmt.Errorf("decode open library response: %w", err) } return nil } func extractOLDescription(desc interface{}) string { switch v := desc.(type) { case string: return v case map[string]interface{}: if val, ok := v["value"].(string); ok { return val } } return "" } func (p *OpenLibraryProvider) Search(ctx context.Context, query string, opts SearchOptions) ([]MetadataSearchResult, error) { url := fmt.Sprintf("%s/search.json?q=%s&limit=20", p.baseURL, query) if opts.Year != nil { url += fmt.Sprintf("&first_publish_year=%d", *opts.Year) } var resp olSearchResponse if err := p.fetch(ctx, url, &resp); err != nil { return nil, fmt.Errorf("search open library: %w", err) } var results []MetadataSearchResult for _, doc := range resp.Docs { var year *int if doc.FirstPublishYear != 0 { y := doc.FirstPublishYear year = &y } extIDMap := map[string]string{"openlibrary": doc.Key} if len(doc.ISBN) > 0 { extIDMap["isbn"] = doc.ISBN[0] } extIDs, _ := json.Marshal(extIDMap) authorName := "" if len(doc.AuthorName) > 0 { authorName = doc.AuthorName[0] } results = append(results, MetadataSearchResult{ ProviderID: doc.Key, Title: doc.Title, Year: year, MediaType: "book", OriginalTitle: authorName, ExternalIDs: extIDs, }) } return results, nil } func (p *OpenLibraryProvider) GetDetails(ctx context.Context, id string) (*MetadataDetails, error) { workURL := fmt.Sprintf("%s%s.json", p.baseURL, id) var work olWorkDetail if err := p.fetch(ctx, workURL, &work); err != nil { return nil, fmt.Errorf("get open library work: %w", err) } extIDs, _ := json.Marshal(map[string]string{"openlibrary": id}) description := extractOLDescription(work.Description) authorNames := []string{} for _, aRef := range work.Authors { authorURL := fmt.Sprintf("%s%s.json", p.baseURL, aRef.Author.Key) var author olAuthorDetail if err := p.fetch(ctx, authorURL, &author); err == nil { authorNames = append(authorNames, author.Name) } } metadata, _ := json.Marshal(map[string]interface{}{ "authors": authorNames, "covers": work.Covers, }) overview := description return &MetadataDetails{ ProviderID: id, Title: work.Title, Overview: &overview, ExternalIDs: extIDs, Metadata: metadata, }, nil } func (p *OpenLibraryProvider) GetImages(ctx context.Context, id string) ([]ImageResult, error) { workURL := fmt.Sprintf("%s%s.json", p.baseURL, id) var work olWorkDetail if err := p.fetch(ctx, workURL, &work); err != nil { return nil, nil } if len(work.Covers) == 0 { return nil, nil } coverID := work.Covers[0] return []ImageResult{{ URL: fmt.Sprintf("https://covers.openlibrary.org/b/id/%d-L.jpg", coverID), Type: "cover", }}, nil }