200 lines
4.7 KiB
Go
200 lines
4.7 KiB
Go
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
|
|
}
|