diff --git a/cmd/serve/api.go b/cmd/serve/api.go index df8ce9f..e2c892a 100644 --- a/cmd/serve/api.go +++ b/cmd/serve/api.go @@ -2,8 +2,10 @@ package main import ( "encoding/json" + "fmt" "io" "io/fs" + "log" "net/http" "git.yetaga.in/alazyreader/library/media" @@ -30,6 +32,7 @@ func (h path) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func writeJSONerror(w http.ResponseWriter, err string, status int) { + log.Println(err) writeJSON(w, struct{ Status, Reason string }{Status: "error", Reason: err}, status) } @@ -97,50 +100,26 @@ func getBooks(l Library, w http.ResponseWriter, r *http.Request) { } func addBook(l Library, w http.ResponseWriter, r *http.Request) { - if r.Body == nil { - writeJSONerror(w, "no body provided", http.StatusBadRequest) + book, err := ReadBody[media.Book](r.Body) + if err != nil { + writeJSONerror(w, err.Error(), http.StatusBadRequest) return } - defer r.Body.Close() - b, err := io.ReadAll(r.Body) - if err != nil { - writeJSONerror(w, "error reading body", http.StatusBadRequest) - return - } - book := &media.Book{} - err = json.Unmarshal(b, book) - if err != nil { - writeJSONerror(w, "error parsing body", http.StatusBadRequest) - return - } - err = l.AddBook(r.Context(), book) - if err != nil { - writeJSONerror(w, "error parsing body", http.StatusBadRequest) + if err = l.AddBook(r.Context(), book); err != nil { + writeJSONerror(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusAccepted) } func deleteBook(l Library, w http.ResponseWriter, r *http.Request) { - if r.Body == nil { - writeJSONerror(w, "no body provided", http.StatusBadRequest) + book, err := ReadBody[media.Book](r.Body) + if err != nil { + writeJSONerror(w, err.Error(), http.StatusBadRequest) return } - defer r.Body.Close() - b, err := io.ReadAll(r.Body) - if err != nil { - writeJSONerror(w, "error reading body", http.StatusBadRequest) - return - } - book := &media.Book{} - err = json.Unmarshal(b, book) - if err != nil { - writeJSONerror(w, "error parsing body", http.StatusBadRequest) - return - } - err = l.DeleteBook(r.Context(), book) - if err != nil { - writeJSONerror(w, "error deleting book", http.StatusInternalServerError) + if err = l.DeleteBook(r.Context(), book); err != nil { + writeJSONerror(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusAccepted) @@ -165,12 +144,12 @@ func getWhoAmI(ts *tailscale.LocalClient, w http.ResponseWriter, r *http.Request } 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) + req, err := ReadBody[media.Book](r.Body) + if err != nil { + writeJSONerror(w, err.Error(), http.StatusBadRequest) return } - book, err := query.GetByISBN(isbn) + book, err := query.GetByISBN(req.ISBN13) if err != nil { writeJSONerror(w, err.Error(), http.StatusInternalServerError) return @@ -181,3 +160,20 @@ func lookupBook(query Query, w http.ResponseWriter, r *http.Request) { func static(f fs.FS) http.Handler { return http.FileServer(http.FS(f)) } + +func ReadBody[T any](r io.ReadCloser) (*T, error) { + t := new(T) + if r == nil { + return t, fmt.Errorf("no body provided") + } + defer r.Close() + b, err := io.ReadAll(r) + if err != nil { + return t, fmt.Errorf("error reading body: %w", err) + } + err = json.Unmarshal(b, t) + if err != nil { + return t, fmt.Errorf("error reading body: %w", err) + } + return t, nil +} diff --git a/cmd/serve/main.go b/cmd/serve/main.go index dd20ff1..adb6471 100644 --- a/cmd/serve/main.go +++ b/cmd/serve/main.go @@ -45,50 +45,44 @@ func main() { must.Do(envconfig.Process("library", &c)) var lib Library - var err error if c.DBType == "memory" { lib = &database.Memory{} } else if c.DBType == "sql" { - var latest, run int - lib, latest, run, err = setupSQL(c) + sqllib, latest, run, err := setupSQL(c) if err != nil { - log.Fatalf("err starting sql connection: %v", err) + log.Fatalf("sql connection err: %v", err) } log.Printf("latest migration: %d; migrations run: %d", latest, run) + lib = sqllib } discogsCache := must.Get(database.NewDiscogsCache( 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) errGroup := errgroup.Group{} errGroup.Go(func() error { - return start(servers)( - publicServer(8080, &Router{ - static: staticRoot, - lib: lib, - rcol: discogsCache, - isAdmin: false, - })) + return start(servers)(publicServer(8080, &Router{ + static: staticRoot, + lib: lib, + rcol: discogsCache, + isAdmin: false, + })) }) errGroup.Go(func() error { - return start(servers)( - tailscaleListener("library-admin", &Router{ - static: staticRoot, - lib: lib, - rcol: discogsCache, - query: queryProvider, - isAdmin: true, - })) + return start(servers)(tailscaleListener("library-admin", &Router{ + static: staticRoot, + lib: lib, + rcol: discogsCache, + query: queryProvider, + isAdmin: true, + })) }) errGroup.Go(func() error { return shutdown(servers) }) - log.Println(errGroup.Wait()) } @@ -172,6 +166,5 @@ func tailscaleListener(hostname string, handler *Router) (*http.Server, net.List return nil, nil, err } log.Printf("management server: http://%s/", hostname) - server := &http.Server{Handler: handler} - return server, ln, nil + return &http.Server{Handler: handler}, ln, nil } diff --git a/frontend/files/app.js b/frontend/files/app.js index 8e2a59b..e3b4431 100644 --- a/frontend/files/app.js +++ b/frontend/files/app.js @@ -51,6 +51,62 @@ function init() { function renderAddBookView() { document.getElementById("current").innerHTML = AddBookTemplate(); + document.getElementById("lookup").addEventListener("click", (e) => { + e.preventDefault(); + if (document.getElementById("isbn-13").value.length === 13) { + getPossibleBooks(document.getElementById("isbn-13").value); + } else { + console.log("no isbn"); + } + }); + document.getElementById("save").addEventListener("click", (e) => { + e.preventDefault(); + fetch("/api/books", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + title: document.getElementById("title").value, + authors: document.getElementById("authors").value.split(";"), + sortAuthor: document.getElementById("sortAuthor").value, + "isbn-10": document.getElementById("isbn-10").value, + "isbn-13": document.getElementById("isbn-13").value, + publisher: document.getElementById("publisher").value, + format: document.getElementById("format").value, + genre: document.getElementById("genre").value, + series: document.getElementById("series").value, + volume: document.getElementById("volume").value, + year: document.getElementById("year").value, + coverURL: document.getElementById("coverURL").value, + }), + }); + renderAddBookView(); + init(); + }); +} + +function getPossibleBooks(isbn) { + fetch("/api/query", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ "isbn-13": isbn }), + }) + .then((response) => response.json()) + .then((json) => { + Object.keys(json).forEach((key) => { + var elem = document.getElementById(key); + if (elem !== null) { + elem.value = json[key]; + } + }); + }); +} + +function saveBook(book) { + fetch("/api/books", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(book), + }); } function renderTable(books, sortField) { @@ -94,7 +150,8 @@ function renderTable(books, sortField) { Array.from(bookElement.querySelectorAll("tbody tr th[data-sort-by]")).forEach( (row) => { row.addEventListener("click", function (e) { - renderTable(books, e.target.dataset.sortBy); // only add callback when there's a sortBy attribute + // only add callback when there's a sortBy attribute + renderTable(books, e.target.dataset.sortBy); }); } ); @@ -170,9 +227,7 @@ function BookTemplate({ "isbn-10": isbn10, authors, coverURL, - description, format, - notes, publisher, series, signed, @@ -235,28 +290,33 @@ function TableTemplate(books) { function AddBookTemplate() { return `