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:
@@ -107,6 +107,7 @@
|
|||||||
<h3 class="subtitle">{{data.author}}</h3>
|
<h3 class="subtitle">{{data.author}}</h3>
|
||||||
<p>{{data.summary}}</p>
|
<p>{{data.summary}}</p>
|
||||||
<div class="my-5" v-if="data.isbn">ISBN: {{data.isbn}}</div>
|
<div class="my-5" v-if="data.isbn">ISBN: {{data.isbn}}</div>
|
||||||
|
<div class="my-5" v-if="data.openlibraryid">OLID: {{data.openlibraryid}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="iconscontainer" :class="data.read ? 'remove-border-bottom' : ''">
|
<div class="iconscontainer" :class="data.read ? 'remove-border-bottom' : ''">
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ function openBook() {
|
|||||||
if (props.id != 0) {
|
if (props.id != 0) {
|
||||||
router.push(`/book/${props.id}`);
|
router.push(`/book/${props.id}`);
|
||||||
} else {
|
} else {
|
||||||
console.log("Open Library Id : " + props.openlibraryid);
|
router.push(`/importopenlibrary/${props.openlibraryid}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
32
front/src/OpenLibraryImport.vue
Normal file
32
front/src/OpenLibraryImport.vue
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { postImportBook } from './api.js'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
openlibraryid: String
|
||||||
|
});
|
||||||
|
const error = ref(null);
|
||||||
|
const data = ref(null);
|
||||||
|
|
||||||
|
async function importOpenLibraryId() {
|
||||||
|
const res = await postImportBook(props.openlibraryid);
|
||||||
|
const json = await res.json();
|
||||||
|
if (res.ok) {
|
||||||
|
router.push(`/book/${json.id}`);
|
||||||
|
} else {
|
||||||
|
error.value = json;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
importOpenLibraryId();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="error">Importing {{props.openlibraryid}}...</div>
|
||||||
|
<div v-else-if="!data">Importing {{props.openlibraryid}}...</div>
|
||||||
|
</template>
|
||||||
|
<style scoped></style>
|
||||||
|
|
||||||
@@ -62,6 +62,10 @@ export function postBook(book) {
|
|||||||
return genericPayloadCall('/book', book.value, 'POST')
|
return genericPayloadCall('/book', book.value, 'POST')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function postImportBook(id) {
|
||||||
|
return genericPayloadCall('/importbook', {openlibraryid: id}, 'POST');
|
||||||
|
}
|
||||||
|
|
||||||
export async function putReadBook(bookId) {
|
export async function putReadBook(bookId) {
|
||||||
return genericPayloadCall('/book/' + bookId + "/read", {read: true}, 'PUT')
|
return genericPayloadCall('/book/' + bookId + "/read", {read: true}, 'PUT')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ import SignUp from './SignUp.vue'
|
|||||||
import LogIn from './LogIn.vue'
|
import LogIn from './LogIn.vue'
|
||||||
import Home from './Home.vue'
|
import Home from './Home.vue'
|
||||||
import SearchBook from './SearchBook.vue'
|
import SearchBook from './SearchBook.vue'
|
||||||
|
import OpenLibraryImport from './OpenLibraryImport.vue'
|
||||||
import { useAuthStore } from './auth.store'
|
import { useAuthStore } from './auth.store'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{ path: '/', component: Home },
|
{ path: '/', component: Home },
|
||||||
{ path: '/books', component: BooksBrowser },
|
{ path: '/books', component: BooksBrowser },
|
||||||
{ path: '/book/:id', component: BookForm, props: true },
|
{ path: '/book/:id', component: BookForm, props: true },
|
||||||
|
{ path: '/importopenlibrary/:openlibraryid', component: OpenLibraryImport, props: true },
|
||||||
{ path: '/author/:id', component: AuthorForm, props: true },
|
{ path: '/author/:id', component: AuthorForm, props: true },
|
||||||
{ path: '/search/:searchterm', component: SearchBook, props: true },
|
{ path: '/search/:searchterm', component: SearchBook, props: true },
|
||||||
{ path: '/add', component: AddBook },
|
{ path: '/add', component: AddBook },
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type fetchedBook struct {
|
|||||||
Title string `json:"title" binding:"required,max=300"`
|
Title string `json:"title" binding:"required,max=300"`
|
||||||
Author string `json:"author" binding:"max=100"`
|
Author string `json:"author" binding:"max=100"`
|
||||||
ISBN string `json:"isbn"`
|
ISBN string `json:"isbn"`
|
||||||
|
OpenLibraryId string `json:"openlibraryid"`
|
||||||
Summary string `json:"summary"`
|
Summary string `json:"summary"`
|
||||||
Rating int `json:"rating"`
|
Rating int `json:"rating"`
|
||||||
Read bool `json:"read"`
|
Read bool `json:"read"`
|
||||||
|
|||||||
66
internal/apitest/post_importbook_test.go
Normal file
66
internal/apitest/post_importbook_test.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package apitest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.artlef.fr/PersonalLibraryManager/internal/testutils"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type hasId struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostImportBookHandler_Ok(t *testing.T) {
|
||||||
|
id := testPostImportBookHandler(t, "OL21177W", http.StatusOK)
|
||||||
|
book := testGetBook(t, strconv.FormatUint(uint64(id), 10), 200)
|
||||||
|
assert.Equal(t, "Wuthering Heights", book.Title)
|
||||||
|
assert.Equal(t, "Emily Brontë", book.Author)
|
||||||
|
assert.Equal(t, "OL21177W", book.OpenLibraryId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostImportBookHandler_OkAuthorKey(t *testing.T) {
|
||||||
|
id := testPostImportBookHandler(t, "OL7525169M", http.StatusOK)
|
||||||
|
book := testGetBook(t, strconv.FormatUint(uint64(id), 10), 200)
|
||||||
|
assert.Equal(t, "Dr. Bloodmoney, or How We Got Along After the Bomb", book.Title)
|
||||||
|
assert.Equal(t, "Philip K. Dick", book.Author)
|
||||||
|
assert.Equal(t, "OL7525169M", book.OpenLibraryId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostImportBookHandler_NoOLID(t *testing.T) {
|
||||||
|
testPostImportBookHandler(t, "", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPostImportBookHandler(t *testing.T, openlibraryid string, expectedCode int) uint {
|
||||||
|
router := testutils.TestSetup()
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
token := testutils.ConnectDemoUser(router)
|
||||||
|
queryJson := `{
|
||||||
|
"openlibraryid":"%s"
|
||||||
|
}`
|
||||||
|
queryJson = fmt.Sprintf(queryJson, openlibraryid)
|
||||||
|
req, _ := http.NewRequest("POST", "/importbook",
|
||||||
|
strings.NewReader(string(queryJson)))
|
||||||
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, expectedCode, w.Code)
|
||||||
|
|
||||||
|
if w.Code == 200 {
|
||||||
|
var parsedId hasId
|
||||||
|
err := json.Unmarshal(w.Body.Bytes(), &parsedId)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
return parsedId.ID
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import "gorm.io/gorm"
|
|||||||
|
|
||||||
type Author struct {
|
type Author struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
|
OpenLibraryId string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,61 +2,44 @@ package openlibrary
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type OpenLibraryQueryResult struct {
|
func computeOpenLibraryUrl(paths ...string) (*url.URL, error) {
|
||||||
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
|
|
||||||
baseUrl := "https://openlibrary.org"
|
baseUrl := "https://openlibrary.org"
|
||||||
booksApiUrl := "search.json"
|
|
||||||
u, err := url.Parse(baseUrl)
|
u, err := url.Parse(baseUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return queryResult, err
|
return nil, err
|
||||||
}
|
}
|
||||||
u = u.JoinPath(booksApiUrl)
|
for _, p := range paths {
|
||||||
if limit != 0 {
|
u = u.JoinPath(p)
|
||||||
addQueryParamInt(u, "limit", limit)
|
|
||||||
}
|
}
|
||||||
if offset != 0 {
|
return u, nil
|
||||||
addQueryParamInt(u, "offset", offset)
|
}
|
||||||
}
|
|
||||||
addQueryParam(u, "q", searchterm)
|
func fetchAndParseResult[T any](u *url.URL, queryResult *T) error {
|
||||||
client := &http.Client{}
|
resp, err := doOpenLibraryQuery(u)
|
||||||
req, err := http.NewRequest("GET", u.String(), nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return queryResult, err
|
return 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
|
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
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 {
|
if err != nil {
|
||||||
return queryResult, err
|
return nil, err
|
||||||
}
|
}
|
||||||
bodyString := string(bodyBytes)
|
req.Header.Add("Accept", "application/json")
|
||||||
decoder := json.NewDecoder(strings.NewReader(bodyString))
|
req.Header.Add("User-Agent", "PersonalLibraryManager/0.1 (artlef@protonmail.com)")
|
||||||
err = decoder.Decode(&queryResult)
|
return client.Do(req)
|
||||||
return queryResult, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func addQueryParamInt(u *url.URL, paramName string, paramValue int) {
|
func addQueryParamInt(u *url.URL, paramName string, paramValue int) {
|
||||||
|
|||||||
@@ -7,21 +7,61 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCallOpenLibrary(t *testing.T) {
|
func TestCallOpenLibrarySearch(t *testing.T) {
|
||||||
result, err := openlibrary.CallOpenLibrary("man in high castle", 0, 0)
|
result, err := openlibrary.CallOpenLibrarySearch("man in high castle", 0, 0)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, result.NumFound, 25)
|
assert.Equal(t, 25, result.NumFound)
|
||||||
assert.Equal(t, len(result.Books), 25)
|
assert.Equal(t, 25, len(result.Books))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCallOpenLibraryLimit(t *testing.T) {
|
func TestCallOpenLibrarySearchLimit(t *testing.T) {
|
||||||
result, err := openlibrary.CallOpenLibrary("man in high castle", 10, 0)
|
result, err := openlibrary.CallOpenLibrarySearch("man in high castle", 10, 0)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, len(result.Books), 10)
|
assert.Equal(t, 10, len(result.Books))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCallOpenLibraryOffset(t *testing.T) {
|
func TestCallOpenLibrarySearchOffset(t *testing.T) {
|
||||||
result, err := openlibrary.CallOpenLibrary("man in high castle", 0, 10)
|
result, err := openlibrary.CallOpenLibrarySearch("man in high castle", 0, 10)
|
||||||
assert.Nil(t, err)
|
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
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ type BookGet struct {
|
|||||||
Title string `json:"title" binding:"required,max=300"`
|
Title string `json:"title" binding:"required,max=300"`
|
||||||
Author string `json:"author" binding:"max=100"`
|
Author string `json:"author" binding:"max=100"`
|
||||||
ISBN string `json:"isbn"`
|
ISBN string `json:"isbn"`
|
||||||
|
OpenLibraryId string `json:"openlibraryid"`
|
||||||
Summary string `json:"summary"`
|
Summary string `json:"summary"`
|
||||||
Rating int `json:"rating"`
|
Rating int `json:"rating"`
|
||||||
Read bool `json:"read"`
|
Read bool `json:"read"`
|
||||||
@@ -22,7 +23,7 @@ type BookGet struct {
|
|||||||
func FetchBookGet(db *gorm.DB, userId uint, bookId uint64) (BookGet, error) {
|
func FetchBookGet(db *gorm.DB, userId uint, bookId uint64) (BookGet, error) {
|
||||||
var book BookGet
|
var book BookGet
|
||||||
query := db.Model(&model.Book{})
|
query := db.Model(&model.Book{})
|
||||||
selectQueryString := "books.title, authors.name as author, books.isbn, books.summary, " +
|
selectQueryString := "books.title, authors.name as author, books.isbn, books.open_library_id, books.summary, " +
|
||||||
"user_books.rating, user_books.read, user_books.want_read, " +
|
"user_books.rating, user_books.read, user_books.want_read, " +
|
||||||
"DATE(user_books.start_read_date) as start_read_date, " +
|
"DATE(user_books.start_read_date) as start_read_date, " +
|
||||||
"DATE(user_books.end_read_date) AS end_read_date, " +
|
"DATE(user_books.end_read_date) AS end_read_date, " +
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ func saveBookToDb(ac appcontext.AppContext, b bookPostCreate, user *model.User)
|
|||||||
|
|
||||||
func fetchOrCreateAuthor(ac appcontext.AppContext, name string) (*model.Author, error) {
|
func fetchOrCreateAuthor(ac appcontext.AppContext, name string) (*model.Author, error) {
|
||||||
var author model.Author
|
var author model.Author
|
||||||
res := ac.Db.Where("name = ?", name)
|
res := ac.Db.Where("name = ?", name).First(&author)
|
||||||
err := res.Error
|
err := res.Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
|||||||
107
internal/routes/bookpostimport.go
Normal file
107
internal/routes/bookpostimport.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"git.artlef.fr/PersonalLibraryManager/internal/appcontext"
|
||||||
|
"git.artlef.fr/PersonalLibraryManager/internal/model"
|
||||||
|
"git.artlef.fr/PersonalLibraryManager/internal/myvalidator"
|
||||||
|
"git.artlef.fr/PersonalLibraryManager/internal/openlibrary"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type bookPostImport struct {
|
||||||
|
OpenLibraryId string `json:"openlibraryid" binding:"required,max=50"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func PostImportBookHandler(ac appcontext.AppContext) {
|
||||||
|
var request bookPostImport
|
||||||
|
err := ac.C.ShouldBindJSON(&request)
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, fetchUserErr := ac.GetAuthenticatedUser()
|
||||||
|
if fetchUserErr != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
openLibraryBook, err := openlibrary.CallOpenLibraryBook(request.OpenLibraryId)
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
book, err := saveOpenLibraryBookToDb(ac, request.OpenLibraryId, openLibraryBook, &user)
|
||||||
|
if err != nil {
|
||||||
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ac.C.JSON(200, gin.H{"id": book.ID})
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveOpenLibraryBookToDb(ac appcontext.AppContext, openLibraryId string, openLibraryBook openlibrary.OpenLibraryBookResult, user *model.User) (*model.Book, error) {
|
||||||
|
author, err := fetchOrCreateOpenLibraryAuthor(ac, openLibraryBook.AuthorID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
book := model.Book{
|
||||||
|
Title: openLibraryBook.Title,
|
||||||
|
Summary: openLibraryBook.Description,
|
||||||
|
OpenLibraryId: openLibraryId,
|
||||||
|
Author: *author,
|
||||||
|
AddedBy: *user,
|
||||||
|
}
|
||||||
|
err = ac.Db.Save(&book).Error
|
||||||
|
return &book, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchOrCreateOpenLibraryAuthor(ac appcontext.AppContext, openlibraryAuthorId string) (*model.Author, error) {
|
||||||
|
var author model.Author
|
||||||
|
res := ac.Db.Where("open_library_id = ?", openlibraryAuthorId).First(&author)
|
||||||
|
err := res.Error
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
newAuthor, err := createAuthorFromOpenLibrary(ac, openlibraryAuthorId)
|
||||||
|
if err != nil {
|
||||||
|
return &author, err
|
||||||
|
}
|
||||||
|
return newAuthor, nil
|
||||||
|
} else {
|
||||||
|
return &author, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return &author, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createAuthorFromOpenLibrary(ac appcontext.AppContext, openlibraryAuthorId string) (*model.Author, error) {
|
||||||
|
authorFromOL, err := openlibrary.CallOpenLibraryAuthor(openlibraryAuthorId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var author model.Author
|
||||||
|
res := ac.Db.Where("name = ?", authorFromOL.Name).First(&author)
|
||||||
|
err = res.Error
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
author = model.Author{
|
||||||
|
Name: authorFromOL.Name,
|
||||||
|
Description: authorFromOL.Description,
|
||||||
|
OpenLibraryId: openlibraryAuthorId,
|
||||||
|
}
|
||||||
|
err = ac.Db.Save(&author).Error
|
||||||
|
return &author, err
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//if the author already exists, only fill the open library id
|
||||||
|
author.OpenLibraryId = openlibraryAuthorId
|
||||||
|
ac.Db.Save(&author)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &author, err
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ func GetSearchBooksHandler(ac appcontext.AppContext) {
|
|||||||
if len(books) > 0 {
|
if len(books) > 0 {
|
||||||
returnedBooks = books
|
returnedBooks = books
|
||||||
} else {
|
} else {
|
||||||
queryResult, err := openlibrary.CallOpenLibrary(searchterm, limit, offset)
|
queryResult, err := openlibrary.CallOpenLibrarySearch(searchterm, limit, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
return
|
return
|
||||||
@@ -48,7 +48,7 @@ func GetSearchBooksHandler(ac appcontext.AppContext) {
|
|||||||
ac.C.JSON(http.StatusOK, returnedBooks)
|
ac.C.JSON(http.StatusOK, returnedBooks)
|
||||||
}
|
}
|
||||||
|
|
||||||
func OpenLibraryBooksToBookSearchGet(OLbooks []openlibrary.OpenLibraryBook) []query.BookSearchGet {
|
func OpenLibraryBooksToBookSearchGet(OLbooks []openlibrary.OpenLibrarySearchBook) []query.BookSearchGet {
|
||||||
var books []query.BookSearchGet
|
var books []query.BookSearchGet
|
||||||
for _, b := range OLbooks {
|
for _, b := range OLbooks {
|
||||||
bookSearchGet := query.BookSearchGet{
|
bookSearchGet := query.BookSearchGet{
|
||||||
@@ -83,7 +83,7 @@ func GetSearchBooksCountHandler(ac appcontext.AppContext) {
|
|||||||
if count > 0 {
|
if count > 0 {
|
||||||
finalCount = count
|
finalCount = count
|
||||||
} else {
|
} else {
|
||||||
queryResult, err := openlibrary.CallOpenLibrary(searchterm, 0, 0)
|
queryResult, err := openlibrary.CallOpenLibrarySearch(searchterm, 0, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
myvalidator.ReturnErrorsAsJsonResponse(&ac, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -66,6 +66,9 @@ func Setup(config *config.Config) *gin.Engine {
|
|||||||
r.POST("/book", func(c *gin.Context) {
|
r.POST("/book", func(c *gin.Context) {
|
||||||
routes.PostBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
routes.PostBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||||
})
|
})
|
||||||
|
r.POST("/importbook", func(c *gin.Context) {
|
||||||
|
routes.PostImportBookHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||||
|
})
|
||||||
r.GET("/author/:id", func(c *gin.Context) {
|
r.GET("/author/:id", func(c *gin.Context) {
|
||||||
routes.GetAuthorHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
routes.GetAuthorHandler(appcontext.AppContext{C: c, Db: db, I18n: bundle, Config: config})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user