Open Library search API
If nothing is found on the server, returns results from open library API. Clicking on a book imports it.
This commit is contained in:
@@ -2,61 +2,44 @@ package openlibrary
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type OpenLibraryQueryResult struct {
|
||||
Books []OpenLibraryBook `json:"docs"`
|
||||
NumFound int `json:"numFound"`
|
||||
}
|
||||
|
||||
type OpenLibraryBook struct {
|
||||
Title string `json:"title"`
|
||||
Authors []string `json:"author_name"`
|
||||
OpenLibraryId string `json:"cover_edition_key"`
|
||||
}
|
||||
|
||||
func CallOpenLibrary(searchterm string, limit int, offset int) (OpenLibraryQueryResult, error) {
|
||||
var queryResult OpenLibraryQueryResult
|
||||
func computeOpenLibraryUrl(paths ...string) (*url.URL, error) {
|
||||
baseUrl := "https://openlibrary.org"
|
||||
booksApiUrl := "search.json"
|
||||
u, err := url.Parse(baseUrl)
|
||||
if err != nil {
|
||||
return queryResult, err
|
||||
return nil, err
|
||||
}
|
||||
u = u.JoinPath(booksApiUrl)
|
||||
if limit != 0 {
|
||||
addQueryParamInt(u, "limit", limit)
|
||||
for _, p := range paths {
|
||||
u = u.JoinPath(p)
|
||||
}
|
||||
if offset != 0 {
|
||||
addQueryParamInt(u, "offset", offset)
|
||||
}
|
||||
addQueryParam(u, "q", searchterm)
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", u.String(), nil)
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func fetchAndParseResult[T any](u *url.URL, queryResult *T) error {
|
||||
resp, err := doOpenLibraryQuery(u)
|
||||
if err != nil {
|
||||
return queryResult, err
|
||||
}
|
||||
req.Header.Add("Accept", "application/json")
|
||||
req.Header.Add("User-Agent", "PersonalLibraryManager/0.1 (artlef@protonmail.com)")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return queryResult, err
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
err = decoder.Decode(queryResult)
|
||||
return err
|
||||
}
|
||||
|
||||
func doOpenLibraryQuery(u *url.URL) (*http.Response, error) {
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", u.String(), nil)
|
||||
if err != nil {
|
||||
return queryResult, err
|
||||
return nil, err
|
||||
}
|
||||
bodyString := string(bodyBytes)
|
||||
decoder := json.NewDecoder(strings.NewReader(bodyString))
|
||||
err = decoder.Decode(&queryResult)
|
||||
return queryResult, err
|
||||
req.Header.Add("Accept", "application/json")
|
||||
req.Header.Add("User-Agent", "PersonalLibraryManager/0.1 (artlef@protonmail.com)")
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
func addQueryParamInt(u *url.URL, paramName string, paramValue int) {
|
||||
|
||||
@@ -7,21 +7,61 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCallOpenLibrary(t *testing.T) {
|
||||
result, err := openlibrary.CallOpenLibrary("man in high castle", 0, 0)
|
||||
func TestCallOpenLibrarySearch(t *testing.T) {
|
||||
result, err := openlibrary.CallOpenLibrarySearch("man in high castle", 0, 0)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, result.NumFound, 25)
|
||||
assert.Equal(t, len(result.Books), 25)
|
||||
assert.Equal(t, 25, result.NumFound)
|
||||
assert.Equal(t, 25, len(result.Books))
|
||||
}
|
||||
|
||||
func TestCallOpenLibraryLimit(t *testing.T) {
|
||||
result, err := openlibrary.CallOpenLibrary("man in high castle", 10, 0)
|
||||
func TestCallOpenLibrarySearchLimit(t *testing.T) {
|
||||
result, err := openlibrary.CallOpenLibrarySearch("man in high castle", 10, 0)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, len(result.Books), 10)
|
||||
assert.Equal(t, 10, len(result.Books))
|
||||
}
|
||||
|
||||
func TestCallOpenLibraryOffset(t *testing.T) {
|
||||
result, err := openlibrary.CallOpenLibrary("man in high castle", 0, 10)
|
||||
func TestCallOpenLibrarySearchOffset(t *testing.T) {
|
||||
result, err := openlibrary.CallOpenLibrarySearch("man in high castle", 0, 10)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, len(result.Books), 15)
|
||||
assert.Equal(t, 15, len(result.Books))
|
||||
}
|
||||
|
||||
func TestCallOpenLibraryBook_FirstAuthorFormat(t *testing.T) {
|
||||
result, err := openlibrary.CallOpenLibraryBook("OL21177W")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, openlibrary.OpenLibraryBookResult{
|
||||
Title: "Wuthering Heights",
|
||||
Description: "Wuthering Heights is an 1847 novel by Emily Brontë, initially published under the pseudonym Ellis Bell. It concerns two families of the landed gentry living on the West Yorkshire moors, the Earnshaws and the Lintons, and their turbulent relationships with Earnshaw's adopted son, Heathcliff. The novel was influenced by Romanticism and Gothic fiction.",
|
||||
AuthorID: "OL24529A",
|
||||
}, result)
|
||||
}
|
||||
|
||||
func TestCallOpenLibraryBook_SecondAuthorFormat(t *testing.T) {
|
||||
result, err := openlibrary.CallOpenLibraryBook("OL18388220M")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, openlibrary.OpenLibraryBookResult{
|
||||
Title: "Dr Bloodmoney",
|
||||
Description: "",
|
||||
AuthorID: "OL274606A",
|
||||
}, result)
|
||||
}
|
||||
|
||||
func TestCallOpenLibraryAuthor_SimpleBioFormat(t *testing.T) {
|
||||
result, err := openlibrary.CallOpenLibraryAuthor("OL24529A")
|
||||
assert.Nil(t, err)
|
||||
expectedAuthorBio := "Emily Jane Brontë was an English novelist and poet, now best remembered for her novel [Wuthering Heights][1], a classic of English literature. Emily was the second eldest of the three surviving Brontë sisters, between Charlotte and Anne. She published under the androgynous pen name Ellis Bell. ([Source][2].)\r\n\r\n\r\n [1]: http://upstream.openlibrary.org/works/OL10427528W/Wuthering_Heights\r\n [2]: http://en.wikipedia.org/wiki/Emily_Bronte"
|
||||
assert.Equal(t, openlibrary.OpenLibraryAuthorResult{
|
||||
Name: "Emily Brontë",
|
||||
Description: expectedAuthorBio,
|
||||
}, result)
|
||||
}
|
||||
|
||||
func TestCallOpenLibraryAuthor_OtherBioFormat(t *testing.T) {
|
||||
result, err := openlibrary.CallOpenLibraryAuthor("OL274606A")
|
||||
assert.Nil(t, err)
|
||||
expectedAuthorBio := "Philip Kindred Dick was an American novelist, short story writer, and essayist whose published work during his lifetime was almost entirely in the science fiction genre. Dick explored sociological, political and metaphysical themes in novels dominated by monopolistic corporations, authoritarian governments, and altered states. In his later works, Dick's thematic focus strongly reflected his personal interest in metaphysics and theology. He often drew upon his own life experiences and addressed the nature of drug abuse, paranoia and schizophrenia, and transcendental experiences in novels such as A Scanner Darkly and VALIS.\r\n\r\nSource and more information: [Wikipedia (EN)](http://en.wikipedia.org/wiki/Philip_K._Dick)"
|
||||
assert.Equal(t, openlibrary.OpenLibraryAuthorResult{
|
||||
Name: "Philip K. Dick",
|
||||
Description: expectedAuthorBio,
|
||||
}, result)
|
||||
}
|
||||
|
||||
58
internal/openlibrary/openlibraryauthor.go
Normal file
58
internal/openlibrary/openlibraryauthor.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package openlibrary
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type OpenLibraryAuthorResult struct {
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
type openLibraryParsedAuthor struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description openLibraryAuthorBio `json:"bio"`
|
||||
}
|
||||
|
||||
type openLibraryAuthorBio struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
func (b *openLibraryAuthorBio) UnmarshalJSON(data []byte) error {
|
||||
var possibleFormat struct {
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
err := json.Unmarshal(data, &possibleFormat)
|
||||
if err != nil {
|
||||
//if unmarshalling failed, try to decode as string
|
||||
var value string
|
||||
err = json.Unmarshal(data, &value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.Value = value
|
||||
} else {
|
||||
b.Value = possibleFormat.Value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CallOpenLibraryAuthor(openLibraryId string) (OpenLibraryAuthorResult, error) {
|
||||
var response OpenLibraryAuthorResult
|
||||
u, err := computeOpenLibraryUrl("authors", fmt.Sprintf("%s.json", openLibraryId))
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
var queryResult openLibraryParsedAuthor
|
||||
err = fetchAndParseResult(u, &queryResult)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
response = OpenLibraryAuthorResult{
|
||||
Name: queryResult.Name,
|
||||
Description: queryResult.Description.Value,
|
||||
}
|
||||
return response, err
|
||||
}
|
||||
93
internal/openlibrary/openlibrarybook.go
Normal file
93
internal/openlibrary/openlibrarybook.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package openlibrary
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type OpenLibraryBookResult struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
AuthorID string `json:"author_id"`
|
||||
}
|
||||
|
||||
type openLibraryParsedBook struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
AuthorInfo []openLibraryBookAuthorResult `json:"authors"`
|
||||
}
|
||||
|
||||
type openLibraryBookAuthorResult struct {
|
||||
AuthorOLID string
|
||||
Type string
|
||||
Key string
|
||||
}
|
||||
|
||||
type keyContainer struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
func (r *openLibraryBookAuthorResult) UnmarshalJSON(data []byte) error {
|
||||
|
||||
var parentAuthor struct {
|
||||
AuthorKey keyContainer `json:"author"`
|
||||
TypeKey keyContainer `json:"type"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &parentAuthor); err != nil {
|
||||
return err
|
||||
}
|
||||
r.AuthorOLID = parseUrlPathToField(parentAuthor.AuthorKey.Key)
|
||||
r.Type = parseUrlPathToField(parentAuthor.TypeKey.Key)
|
||||
r.Key = parseUrlPathToField(parentAuthor.Key)
|
||||
return nil
|
||||
}
|
||||
|
||||
// some fields follow this format "/authors/OL274606A"
|
||||
// this method extracts the last part "OL274606A"
|
||||
func parseUrlPathToField(urlpath string) string {
|
||||
splittedPath := strings.Split(urlpath, "/")
|
||||
if len(splittedPath) > 2 {
|
||||
return splittedPath[2]
|
||||
} else {
|
||||
return urlpath
|
||||
}
|
||||
}
|
||||
|
||||
func CallOpenLibraryBook(openLibraryId string) (OpenLibraryBookResult, error) {
|
||||
var response OpenLibraryBookResult
|
||||
u, err := computeOpenLibraryUrl("works", fmt.Sprintf("%s.json", openLibraryId))
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
var queryResult openLibraryParsedBook
|
||||
err = fetchAndParseResult(u, &queryResult)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
author := computeAuthorFromParsedBook(&queryResult)
|
||||
response = OpenLibraryBookResult{
|
||||
Title: queryResult.Title,
|
||||
Description: queryResult.Description,
|
||||
AuthorID: author,
|
||||
}
|
||||
return response, err
|
||||
}
|
||||
|
||||
func computeAuthorFromParsedBook(parsedBook *openLibraryParsedBook) string {
|
||||
authors := parsedBook.AuthorInfo
|
||||
for _, author := range authors {
|
||||
if author.Key != "" {
|
||||
return author.Key
|
||||
}
|
||||
if author.Type == "author_role" {
|
||||
return author.AuthorOLID
|
||||
}
|
||||
}
|
||||
if len(authors) > 0 {
|
||||
return authors[0].AuthorOLID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
29
internal/openlibrary/openlibrarysearch.go
Normal file
29
internal/openlibrary/openlibrarysearch.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package openlibrary
|
||||
|
||||
type OpenLibrarySearchResult struct {
|
||||
Books []OpenLibrarySearchBook `json:"docs"`
|
||||
NumFound int `json:"numFound"`
|
||||
}
|
||||
|
||||
type OpenLibrarySearchBook struct {
|
||||
Title string `json:"title"`
|
||||
Authors []string `json:"author_name"`
|
||||
OpenLibraryId string `json:"cover_edition_key"`
|
||||
}
|
||||
|
||||
func CallOpenLibrarySearch(searchterm string, limit int, offset int) (OpenLibrarySearchResult, error) {
|
||||
var queryResult OpenLibrarySearchResult
|
||||
u, err := computeOpenLibraryUrl("search.json")
|
||||
if err != nil {
|
||||
return queryResult, err
|
||||
}
|
||||
if limit != 0 {
|
||||
addQueryParamInt(u, "limit", limit)
|
||||
}
|
||||
if offset != 0 {
|
||||
addQueryParamInt(u, "offset", offset)
|
||||
}
|
||||
addQueryParam(u, "q", searchterm)
|
||||
err = fetchAndParseResult(u, &queryResult)
|
||||
return queryResult, err
|
||||
}
|
||||
Reference in New Issue
Block a user