Sync from /srv/compose/unified-media-manager
This commit is contained in:
199
internal/service/openlibrary.go
Normal file
199
internal/service/openlibrary.go
Normal file
@@ -0,0 +1,199 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user