Files
unified-media-manager/internal/service/openlibrary.go
2026-04-24 10:45:19 -07:00

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
}