This commit is contained in:
parent
382d3bab61
commit
86f643ad0b
@ -14,6 +14,7 @@ type Router struct {
|
||||
static fs.FS
|
||||
lib Library
|
||||
rcol RecordCollection
|
||||
query Query
|
||||
ts *tailscale.LocalClient
|
||||
isAdmin bool
|
||||
}
|
||||
@ -73,6 +74,14 @@ func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
p[http.MethodDelete] = func() { deleteBook(router.lib, w, r) }
|
||||
}
|
||||
p.ServeHTTP(w, r)
|
||||
case "/api/query":
|
||||
if !router.isAdmin {
|
||||
http.NotFoundHandler().ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
path{
|
||||
http.MethodPost: func() { lookupBook(router.query, w, r) },
|
||||
}.ServeHTTP(w, r)
|
||||
default:
|
||||
static(router.static).ServeHTTP(w, r)
|
||||
}
|
||||
@ -159,6 +168,20 @@ func getWhoAmI(ts *tailscale.LocalClient, w http.ResponseWriter, r *http.Request
|
||||
writeJSON(w, whois.UserProfile, http.StatusOK)
|
||||
}
|
||||
|
||||
func lookupBook(query Query, w http.ResponseWriter, r *http.Request) {
|
||||
isbn := r.FormValue("isbn")
|
||||
if len(isbn) != 10 && len(isbn) != 13 {
|
||||
writeJSONerror(w, "invalid isbn", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
book, err := query.GetByISBN(isbn)
|
||||
if err != nil {
|
||||
writeJSONerror(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
writeJSON(w, book, http.StatusOK)
|
||||
}
|
||||
|
||||
func static(f fs.FS) http.Handler {
|
||||
return http.FileServer(http.FS(f))
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"git.yetaga.in/alazyreader/library/database"
|
||||
"git.yetaga.in/alazyreader/library/frontend"
|
||||
"git.yetaga.in/alazyreader/library/media"
|
||||
"git.yetaga.in/alazyreader/library/query"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"tailscale.com/tsnet"
|
||||
@ -35,6 +36,10 @@ type RecordCollection interface {
|
||||
GetAllRecords(context.Context) ([]media.Record, error)
|
||||
}
|
||||
|
||||
type Query interface {
|
||||
GetByISBN(string) (*media.Book, error)
|
||||
}
|
||||
|
||||
func main() {
|
||||
var c config.Config
|
||||
must.Do(envconfig.Process("library", &c))
|
||||
@ -55,6 +60,8 @@ func main() {
|
||||
c.DiscogsToken, time.Hour*24, c.DiscogsUser, c.DiscogsPersist, c.DiscogsCacheFile,
|
||||
))
|
||||
|
||||
queryProvider := &query.GoogleBooks{}
|
||||
|
||||
staticRoot := must.Get(frontend.Root())
|
||||
|
||||
servers := make(chan (*http.Server), 3)
|
||||
@ -74,6 +81,7 @@ func main() {
|
||||
static: staticRoot,
|
||||
lib: lib,
|
||||
rcol: discogsCache,
|
||||
query: queryProvider,
|
||||
isAdmin: true,
|
||||
}))
|
||||
})
|
||||
@ -137,11 +145,11 @@ func shutdown(servers chan (*http.Server)) error {
|
||||
|
||||
func publicServer(port int, handler http.Handler) (*http.Server, net.Listener, error) {
|
||||
server := &http.Server{Handler: handler}
|
||||
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", 8080))
|
||||
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
log.Println("starting public server")
|
||||
log.Printf("public server: http://0.0.0.0:%d/", port)
|
||||
return server, ln, nil
|
||||
}
|
||||
|
||||
@ -163,6 +171,7 @@ func tailscaleListener(hostname string, handler *Router) (*http.Server, net.List
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
log.Printf("management server: http://%s/", hostname)
|
||||
server := &http.Server{Handler: handler}
|
||||
return server, ln, nil
|
||||
}
|
||||
|
@ -3,7 +3,23 @@ var sortState = {
|
||||
sortOrder: "asc",
|
||||
};
|
||||
|
||||
var admin = false;
|
||||
|
||||
function init() {
|
||||
fetch("/api/mode")
|
||||
.then((response) => response.json())
|
||||
.then((resp) => (admin = resp.Admin))
|
||||
.then(() => {
|
||||
if (admin) {
|
||||
var element = document.getElementById("addBook");
|
||||
element.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
renderAddBookView();
|
||||
});
|
||||
element.classList.remove("hidden");
|
||||
}
|
||||
});
|
||||
|
||||
fetch("/api/books")
|
||||
.then((response) => response.json())
|
||||
.then((books) => {
|
||||
@ -33,6 +49,10 @@ function init() {
|
||||
});
|
||||
}
|
||||
|
||||
function renderAddBookView() {
|
||||
document.getElementById("current").innerHTML = AddBookTemplate();
|
||||
}
|
||||
|
||||
function renderTable(books, sortField) {
|
||||
if (sortField) {
|
||||
if (sortState.sortBy === sortField && sortState.sortOrder === "asc") {
|
||||
@ -173,6 +193,7 @@ function BookTemplate({
|
||||
}
|
||||
${signed ? "<span>Signed by the author ✒</span><br/>" : ""}
|
||||
<span>${format}</span>
|
||||
${admin ? `<a href="#">Edit Book</a>` : ""}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@ -211,3 +232,31 @@ function TableTemplate(books) {
|
||||
return acc.concat(TableRowTemplate(book));
|
||||
}, "")} </table>`;
|
||||
}
|
||||
|
||||
function AddBookTemplate() {
|
||||
return `<div class="addBookView">
|
||||
<form>${[
|
||||
{ name: "ISBN10", type: "text" },
|
||||
{ name: "ISBN13", type: "text" },
|
||||
{ name: "Title", type: "text" },
|
||||
{ name: "Authors", type: "text" },
|
||||
{ name: "SortAuthor", type: "text" },
|
||||
{ name: "Format", type: "text" },
|
||||
{ name: "Genre", type: "text" },
|
||||
{ name: "Publisher", type: "text" },
|
||||
{ name: "Series", type: "text" },
|
||||
{ name: "Volume", type: "text" },
|
||||
{ name: "Year", type: "text" },
|
||||
{ name: "Signed", type: "checkbox" },
|
||||
// { name: "Description", type: "text" },
|
||||
// { name: "Notes", type: "text" },
|
||||
{ name: "CoverURL", type: "text" },
|
||||
{ name: "Childrens", type: "checkbox" },
|
||||
].reduce((acc, field) => {
|
||||
return acc.concat(
|
||||
`<label>${field.name} <input type="${field.type}" name="${field.name.toLowerCase}"/></label><br/>`
|
||||
);
|
||||
}, "")}
|
||||
</form>
|
||||
</div>`;
|
||||
}
|
||||
|
@ -31,6 +31,7 @@
|
||||
href="https://git.yetaga.in/alazyreader/library"
|
||||
>git</a
|
||||
>
|
||||
<a href="#" id="addBook" class="hidden">add book</a>
|
||||
<div id="searchBox">
|
||||
<label for="childrens" class="bookCount"
|
||||
>Include Childrens Books?</label
|
||||
|
@ -185,7 +185,7 @@ body {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
justify-content: space-evenly;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
overflow: auto;
|
||||
@ -235,4 +235,4 @@ footer {
|
||||
width: calc(100% - 40px);
|
||||
color: #a29c77;
|
||||
border-top: 2px solid #d8d0a0;
|
||||
}
|
||||
}
|
||||
|
@ -133,6 +133,10 @@ body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#header {
|
||||
height: 30px;
|
||||
width: calc(100vw - 20px);
|
||||
|
1
go.mod
1
go.mod
@ -15,6 +15,7 @@ require (
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.0.0 // indirect
|
||||
git.yetaga.in/alazyreader/go-openlibrary v0.0.1 // indirect
|
||||
github.com/akutz/memconn v0.1.0 // indirect
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.21.0 // indirect
|
||||
|
2
go.sum
2
go.sum
@ -2,6 +2,8 @@ filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek=
|
||||
filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
|
||||
filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc=
|
||||
filippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA=
|
||||
git.yetaga.in/alazyreader/go-openlibrary v0.0.1 h1:5juCi8d7YyNxXFvHytQNBww5E6GmPetM7nz3kVUqNQY=
|
||||
git.yetaga.in/alazyreader/go-openlibrary v0.0.1/go.mod h1:o6zBFJTovdFcpA+As1bRFvk5PDhAs2Lf8iVzUt7dKw8=
|
||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
|
||||
|
13
query/amazon.go
Normal file
13
query/amazon.go
Normal file
@ -0,0 +1,13 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.yetaga.in/alazyreader/library/media"
|
||||
)
|
||||
|
||||
type Amazon struct{}
|
||||
|
||||
func (o *Amazon) GetByISBN(isbn string) (*media.Book, error) {
|
||||
return nil, fmt.Errorf("unimplemented")
|
||||
}
|
28
query/funcs.go
Normal file
28
query/funcs.go
Normal file
@ -0,0 +1,28 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func tryGetFirst(s []string) string {
|
||||
if len(s) == 0 {
|
||||
return ""
|
||||
}
|
||||
return s[0]
|
||||
}
|
||||
|
||||
func buildTitle(title, subtitle string) string {
|
||||
if subtitle != "" {
|
||||
return fmt.Sprintf("%s: %s", title, subtitle)
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
func getLastName(author string) string {
|
||||
names := strings.Split(author, " ")
|
||||
if len(names) < 2 {
|
||||
return author
|
||||
}
|
||||
return names[len(names)-1]
|
||||
}
|
158
query/googlebooks.go
Normal file
158
query/googlebooks.go
Normal file
@ -0,0 +1,158 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.yetaga.in/alazyreader/library/media"
|
||||
)
|
||||
|
||||
type GoogleBooks struct{}
|
||||
|
||||
type googleBookResult struct {
|
||||
Kind string `json:"kind"`
|
||||
TotalItems int `json:"totalItems"`
|
||||
Items []item `json:"items"`
|
||||
}
|
||||
|
||||
type industryIdentifier struct {
|
||||
Type string `json:"type"`
|
||||
Identifier string `json:"identifier"`
|
||||
}
|
||||
|
||||
type readingMode struct {
|
||||
Text bool `json:"text"`
|
||||
Image bool `json:"image"`
|
||||
}
|
||||
|
||||
type panelizationSummary struct {
|
||||
ContainsEpubBubbles bool `json:"containsEpubBubbles"`
|
||||
ContainsImageBubbles bool `json:"containsImageBubbles"`
|
||||
}
|
||||
|
||||
type imageLink struct {
|
||||
SmallThumbnail string `json:"smallThumbnail"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
}
|
||||
|
||||
type volumeInfo struct {
|
||||
Title string `json:"title"`
|
||||
Subtitle string `json:"subtitle"`
|
||||
Authors []string `json:"authors"`
|
||||
Publisher string `json:"publisher"`
|
||||
PublishedDate string `json:"publishedDate"`
|
||||
Description string `json:"description"`
|
||||
IndustryIdentifiers []industryIdentifier `json:"industryIdentifiers"`
|
||||
ReadingModes readingMode `json:"readingModes"`
|
||||
PageCount int `json:"pageCount"`
|
||||
PrintType string `json:"printType"`
|
||||
Categories []string `json:"categories"`
|
||||
AverageRating int `json:"averageRating"`
|
||||
RatingsCount int `json:"ratingsCount"`
|
||||
MaturityRating string `json:"maturityRating"`
|
||||
AllowAnonLogging bool `json:"allowAnonLogging"`
|
||||
ContentVersion string `json:"contentVersion"`
|
||||
PanelizationSummary panelizationSummary `json:"panelizationSummary"`
|
||||
ImageLinks imageLink `json:"imageLinks"`
|
||||
Language string `json:"language"`
|
||||
PreviewLink string `json:"previewLink"`
|
||||
InfoLink string `json:"infoLink"`
|
||||
CanonicalVolumeLink string `json:"canonicalVolumeLink"`
|
||||
}
|
||||
|
||||
type saleInfo struct {
|
||||
Country string `json:"country"`
|
||||
Saleability string `json:"saleability"`
|
||||
IsEbook bool `json:"isEbook"`
|
||||
}
|
||||
|
||||
type epub struct {
|
||||
IsAvailable bool `json:"isAvailable"`
|
||||
}
|
||||
|
||||
type pdf struct {
|
||||
IsAvailable bool `json:"isAvailable"`
|
||||
}
|
||||
|
||||
type accessInfo struct {
|
||||
Country string `json:"country"`
|
||||
Viewability string `json:"viewability"`
|
||||
Embeddable bool `json:"embeddable"`
|
||||
PublicDomain bool `json:"publicDomain"`
|
||||
TextToSpeechPermission string `json:"textToSpeechPermission"`
|
||||
Epub epub `json:"epub"`
|
||||
Pdf pdf `json:"pdf"`
|
||||
WebReaderLink string `json:"webReaderLink"`
|
||||
AccessViewStatus string `json:"accessViewStatus"`
|
||||
QuoteSharingAllowed bool `json:"quoteSharingAllowed"`
|
||||
}
|
||||
|
||||
type searchInfo struct {
|
||||
TextSnippet string `json:"textSnippet"`
|
||||
}
|
||||
|
||||
type item struct {
|
||||
Kind string `json:"kind"`
|
||||
ID string `json:"id"`
|
||||
Etag string `json:"etag"`
|
||||
SelfLink string `json:"selfLink"`
|
||||
VolumeInfo volumeInfo `json:"volumeInfo"`
|
||||
SaleInfo saleInfo `json:"saleInfo"`
|
||||
AccessInfo accessInfo `json:"accessInfo"`
|
||||
SearchInfo searchInfo `json:"searchInfo"`
|
||||
}
|
||||
|
||||
func (g *GoogleBooks) GetByISBN(isbn string) (*media.Book, error) {
|
||||
client := &http.Client{}
|
||||
resp, err := client.Get(fmt.Sprintf("https://www.googleapis.com/books/v1/volumes?q=isbn:%s", isbn))
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("received non-200 status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var result googleBookResult
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = json.Unmarshal(b, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(result.Items) == 0 {
|
||||
return nil, fmt.Errorf("no book found")
|
||||
}
|
||||
|
||||
return googleToBook(result.Items[0]), nil
|
||||
}
|
||||
|
||||
func googleToBook(i item) *media.Book {
|
||||
return &media.Book{
|
||||
Title: buildTitle(i.VolumeInfo.Title, i.VolumeInfo.Subtitle),
|
||||
Authors: i.VolumeInfo.Authors,
|
||||
SortAuthor: strings.ToLower(getLastName(tryGetFirst(i.VolumeInfo.Authors))),
|
||||
ISBN10: getIdentifierType(i.VolumeInfo.IndustryIdentifiers, "ISBN_10"),
|
||||
ISBN13: getIdentifierType(i.VolumeInfo.IndustryIdentifiers, "ISBN_13"),
|
||||
Publisher: i.VolumeInfo.Publisher,
|
||||
Year: strings.Split(i.VolumeInfo.PublishedDate, "-")[0],
|
||||
Description: i.VolumeInfo.Description,
|
||||
Genre: tryGetFirst(i.VolumeInfo.Categories),
|
||||
}
|
||||
}
|
||||
|
||||
func getIdentifierType(iis []industryIdentifier, typename string) string {
|
||||
for _, ident := range iis {
|
||||
if ident.Type == typename {
|
||||
return ident.Identifier
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
11
query/null.go
Normal file
11
query/null.go
Normal file
@ -0,0 +1,11 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"git.yetaga.in/alazyreader/library/media"
|
||||
)
|
||||
|
||||
type Null struct{}
|
||||
|
||||
func (o *Null) GetByISBN(isbn string) (*media.Book, error) {
|
||||
return nil, nil
|
||||
}
|
46
query/openlibrary.go
Normal file
46
query/openlibrary.go
Normal file
@ -0,0 +1,46 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.yetaga.in/alazyreader/go-openlibrary/client"
|
||||
"git.yetaga.in/alazyreader/library/media"
|
||||
)
|
||||
|
||||
type OpenLibrary struct {
|
||||
client client.Client
|
||||
}
|
||||
|
||||
func (o *OpenLibrary) GetByISBN(isbn string) (*media.Book, error) {
|
||||
details, err := o.client.GetByISBN(isbn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return openLibraryToBook(details), nil
|
||||
}
|
||||
|
||||
func openLibraryToBook(details *client.BookDetails) *media.Book {
|
||||
return &media.Book{
|
||||
Title: details.Title,
|
||||
Authors: getAuthors(details.Authors),
|
||||
SortAuthor: strings.ToLower(getLastName(tryGetFirst(getAuthors(details.Authors)))),
|
||||
Publisher: getPublisher(details.Publishers),
|
||||
ISBN10: tryGetFirst(details.Identifiers.ISBN10),
|
||||
ISBN13: tryGetFirst(details.Identifiers.ISBN13),
|
||||
}
|
||||
}
|
||||
|
||||
func getPublisher(publishers []client.Publishers) string {
|
||||
if len(publishers) == 0 {
|
||||
return ""
|
||||
}
|
||||
return publishers[0].Name
|
||||
}
|
||||
|
||||
func getAuthors(authors []client.Authors) []string {
|
||||
ret := make([]string, len(authors))
|
||||
for _, author := range authors {
|
||||
ret = append(ret, author.Name)
|
||||
}
|
||||
return ret
|
||||
}
|
Loading…
Reference in New Issue
Block a user