Skip to content

Commit 46b5fa9

Browse files
authored
Merge pull request #52 from seanboyce/dev
Natively handle ISBN search
2 parents db08261 + 119f6d9 commit 46b5fa9

5 files changed

Lines changed: 126 additions & 43 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
# New Features in this Version:
2+
1. Federated search! You can link up ubiblio instances, and search each other's books (but not edit them).
3+
2. An advanced search page. For now, it just handles federated searches.
4+
3. Google Books API returns HTTP 429 errors when no API key is specified. The library used by ubiblio could not correctly set Google Books API keys. So I wrote functions to access (Google Books, OpenLibrary, Wikipedia) so ubiblio no longer relies on external libraries for this.
5+
4. Google books will be skipped on ISBN auto-add unless you specify a Google books API key (free) by setting the environment variable GB_API="YOUR-API-KEY". I'll re-enable it without API key if it starts working again.
26

3-
1. Database backup can now handle files, and the backu/restore UI lets you manage that.
7+
8+
# Notes
9+
10+
The application can be used just fine from the Kindle Paperwhite (and probably most other ebook readers with web browsers), because it is very simple. I find navigating / searching my ebooks gets impractically slow on-device. Ubiblio is much faster, and additionally lets me store all my ebooks elsewhere. No need to ever worry about device storage again! Incidentally, for better display on ebook displays, I've made some text in bold (ebook file management).
11+
12+
# New Features in last Version:
13+
14+
1. Database backup can now handle files, and the backup/restore UI lets you manage that.
415
2. If ISBN auto-adder failes to find a book, try other sources. This can take a little longer, but makes adding books faster overall.
516
3. Clean up book details template -- infrequently used (admin) actions are in an accordion element. Remove that useless 'Back' button.
617
4. Categorize action buttons on book details page
@@ -10,28 +21,9 @@
1021
8. Admin actions should be hidden on all pages for non-admin users (let me know if I've missed one)
1122
9. Added a cursed branch, for deploying on RISCV64. Still no easter eggs, though.
1223

13-
# Notes
14-
15-
The application can be used just fine from the Kindle Paperwhite (and probably most other ebook readers with web browsers), because it is very simple. I find navigating / searching my ebooks gets impractically slow on-device. Ubiblio is much faster, and additionally lets me store all my ebooks elsewhere. No need to ever worry about device storage again! Incidentally, for better display on ebook displays, I've made some text in bold (ebook file management).
16-
17-
# New Features in last Version:
18-
19-
1. Database import / export / restore from a GUI (using the backup script still works though).
20-
2. Database versioning and automatic upgrade (it will back up first).
21-
3. A library config menu for admins, where you can manage backups and other things.
22-
4. You can now judge books by their cover! Image support for up to 16 images per book (enable in config menu -- or if you don't want it, leave it disabled)
23-
5. A luxurious two custom fields you can enable and name at your leisure (from the config menu -- just give them a name to enable)
24-
6. A view for withdrawn books that makes more sense -- you can see who withdrew a book.
25-
7. You can export your book list as CSV in the backup management interface. For example if you just want to use ubiblio for data entry.
26-
8. If you import a book list from ubiblio, is will ADD the books to the database instead of replacing the current database. For example, this lets you merge different libraries.
27-
9. New books no longer default to science fiction! Slightly inconvenient for time travelers, better for the rest of you.
28-
10. Various minor UI improvements.
29-
11. Still zero easter eggs!
30-
3124
# Notes:
3225

33-
1. Be advised that any files you add (book images, backups) are not included in the database backup presently. These are stored in /static/bookImages and /export respectively. In the next major release, files will be backed up and compressed too. For now, you must manually back up these folders.
3426

35-
2. Database upgrade is a little sketchy in this first major update. I haven't been able to break it in testing though. Note that ubiblio will back up your database before attempting to upgrade -- if you encounter a bug with the upgrade process, please reach out.
27+
1. Database upgrade is a little sketchy. I haven't been able to break it in testing though. Note that ubiblio will back up your database before attempting to upgrade -- if you encounter a bug with the upgrade process, please reach out.
3628

3729

SETUP.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ There are a number of environment variables you can set to slightly change the b
6262
| USER_PASSWORD | Password to create | No | - |
6363
| USE_REDIS | Use Redis for DDOS protection? | No | False |
6464
| REDIS_URI | External Redis URI | No | - |
65-
| LANGUAGE | Set the language | No | -|
65+
| LANGUAGE | Set the language | No | - |
66+
| GB_API | Google Books API key | No | - |
6667

6768
For example, to set up an admin user at first launch, you'll need to run a command like below (replacing username and password as you see fit):
6869
```bash
@@ -72,6 +73,19 @@ Then visit /user-setup and the account will be created. It will return you to lo
7273

7374
To set the language to French, launch with LANGUAGE=FR
7475

76+
### Notes on Google Books API Keys
77+
78+
Google Books used to work without API keys. Presently, it does not (reach out if this changes!). To get a free API key for this service, follow the instructions here:
79+
https://developers.google.com/books/docs/v1/getting_started
80+
81+
It's reasonable (but not required) that you restrict the API key to Google Books only. Note that you should keep the API key secret even though the docs at Google say you don't have to -- looks to me like they've had some issues with this recently.
82+
83+
If you don't specify a Google Books API key, ubiblio will simply skip using this service (it's a good data source though!).
84+
85+
Example (note the API key below is not valid):
86+
```bash
87+
env GB_API=AIzaSyAElqyY5RB1EedRnk01vEwkpfhU6aZdrjU uvicorn ubiblio.main:app --host 0.0.0.0 --port 8000 --reload
88+
```
7589
## Launching
7690

7791
There are two supported ways to launch ubiblio -- uvicorn and gunicorn. Both are ridiculously overpowered for such a simple application, but asking why we need such power is a question for philosophers and cowards.

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
fastapi==0.115.4
22
fastapi_limiter==0.1.6
3-
isbnlib==3.10.14
43
passlib==1.7.4
54
pydantic==1.10.2
65
PyMySQL==1.1.1
@@ -15,3 +14,4 @@ python-multipart==0.0.5
1514
aiofiles==24.1.0
1615
pillow==9.0.1
1716
uuid==1.30
17+
requests==2.32.2

ubiblio/main.py

Lines changed: 96 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
from fastapi_limiter.depends import RateLimiter
2424
import json
2525
from datetime import datetime
26-
from isbnlib import *
2726
from .vars import *
2827
import sqlite3
2928
import csv
@@ -33,7 +32,9 @@
3332
import shutil
3433
from ecdsa import SigningKey, VerifyingKey, SECP256k1, BadSignatureError
3534
from hashlib import sha256
36-
35+
import requests
36+
import time
37+
3738
console = Console()
3839
CHUNK_SIZE = 1024 * 1024 #for uploads
3940

@@ -486,6 +487,76 @@ def searchbookTitle(request: Request, user: schemas.User = Depends(get_current_u
486487
except Exception as e:
487488
db.close()
488489
return "An error has occured."
490+
491+
# --------------------------------------------------------------------------
492+
# Book fetch functions, as an alternative to isbnlib
493+
# --------------------------------------------------------------------------
494+
def goobMeta(isbn,key):
495+
url ="https://www.googleapis.com/books/v1/volumes?q=+isbn:"+str(isbn)+"&key="+str(key)
496+
response = requests.get(url)
497+
if response.ok:
498+
rawBook = json.loads(response.text)["items"][0]["volumeInfo"]
499+
book={}
500+
book["Title"]=rawBook["title"]
501+
book["Author"]=rawBook["authors"][0]
502+
try:
503+
book["Summary"] = rawBook["description"]
504+
except:
505+
book["Summary"] = ""
506+
return book, 200
507+
else:
508+
return {},response.status_code
509+
def openLibMeta(isbn):
510+
url = "https://openlibrary.org/isbn/" + str(isbn) + ".json"
511+
headers = {
512+
"User-Agent": 'ubiblio_bot/1.0 (https://github.com/seanboyce/ubiblio;)',
513+
"Accept-Encoding": 'gzip'
514+
} # In case we somehow cause an issue for them, they can contact me
515+
response = requests.get(url,headers=headers)
516+
if response.ok:
517+
rawBook = json.loads(response.text)
518+
book={}
519+
book["Title"]=rawBook["title"]
520+
authorURL = str(rawBook["authors"][0]["key"])
521+
url = "https://openlibrary.org" + authorURL + ".json"
522+
time.sleep(1) #Obey openlibrary ratelimit
523+
response = requests.get(url)
524+
if response.ok:
525+
book["Author"] = json.loads(response.text)["personal_name"]
526+
else:
527+
print(response.status_code)
528+
try:
529+
book["Summary"] = rawBook["description"]["value"]
530+
except:
531+
book["Summary"] = ""
532+
return book, 200
533+
else:
534+
return {},response.status_code
535+
536+
def openWikiMeta(isbn):
537+
url = "https://en.wikipedia.org/api/rest_v1/data/citation/mediawiki/" + str(isbn)
538+
headers = {
539+
"User-Agent": 'ubiblio_bot/1.0 (https://github.com/seanboyce/ubiblio;)',
540+
"Accept-Encoding": 'gzip'
541+
} # In case we somehow cause an issue for them, they can contact me
542+
response = requests.get(url,headers=headers)
543+
if response.ok:
544+
rawBook = json.loads(response.text)[0]
545+
book={}
546+
book["Title"]=rawBook["title"]
547+
rawAuthor = rawBook["author"][0]
548+
author = ""
549+
for i in rawAuthor:
550+
author = author + i + " "
551+
author = author.strip()
552+
book["Author"] = author
553+
book["Summary"] = "" # Summary is always empty on wikipedia
554+
return book, 200
555+
else:
556+
return {}, response.status_code
557+
558+
559+
489560
# --------------------------------------------------------------------------
490561
# ISBN autoadd
491562
# --------------------------------------------------------------------------
@@ -495,36 +566,42 @@ def new_isbn(isbn, method, response: Response, request: Request, user: schemas.U
495566
try:
496567
if user.isAdmin == True:
497568
book = {}
498-
try:
499-
book = meta(isbn,service='goob')
500-
print(book)
501-
except Exception as e:
502-
print("Google Books API failed with response: " )
503-
print(e)
569+
isbn = isbn.strip()
570+
if len(GOOGLE_BOOKS_API_KEY)>0:
571+
try:
572+
book,response = goobMeta(isbn, GOOGLE_BOOKS_API_KEY)
573+
if response != 200:
574+
print("Google Books API failed with response: " )
575+
print(response)
576+
except Exception as e:
577+
print("Google Books API failed with response: " )
578+
print(response)
579+
else:
580+
pass #No google API key defined, so skip google books. Otherwise it's just going to return HTTP 429 error
504581
try:
505582
if len(book)==0:
506-
book = meta(isbn,service="openl")
583+
book,response = openLibMeta(isbn)
584+
if response != 200:
585+
print("Open Library API failed with response: " )
586+
print(response)
507587
else: pass
508588
except Exception as e:
509589
print("Open Library API failed with response: " )
510590
print(e)
511591
try:
512592
if len(book)==0:
513-
book = meta(isbn,service='wiki')
593+
book,response = openWikiMeta(isbn)
594+
if response != 200:
595+
print("Wikipedia API failed with response: " )
596+
print(response)
514597
else: pass
515598
except Exception as e:
516599
print("Wikipedia API failed with response: " )
517600
print(e)
518601
if len(book)==0:
519602
raise LookupError(f"Book with isbn {isbn} not found!")
520-
title = book["Title"]
521-
author = book["Authors"][0]
522-
try:
523-
summary = desc(isbn)
524-
summary = summary.replace('\n', ' ')
525-
except: summary=""
526603
addISBN = [0]
527-
book = schemas.BookCreate(title=title, author=author, summary=summary, ISBN=isbn)
604+
book = schemas.BookCreate(title=book["Title"], author=book["Author"], summary=book["Summary"], ISBN=isbn)
528605
db = SessionLocal()
529606
config = crud.getConfig(db)
530607
db.close()
@@ -1503,7 +1580,7 @@ async def refreshVkey(body: bytes = Depends(get_body), user: schemas.User = Depe
15031580
# --------------------------------------------------------------------------
15041581

15051582
@app.post("/stats", dependencies=[get_rate_limiter(times=2, seconds=2)], response_class=HTMLResponse)
1506-
async def refreshVkey(body: bytes = Depends(get_body), user: schemas.User = Depends(get_current_user_from_token)):
1583+
async def statsp(body: bytes = Depends(get_body), user: schemas.User = Depends(get_current_user_from_token)):
15071584
if not user.isAdmin == True:
15081585
return "You are not authorized to access the library stats page, only Admins can do this."
15091586
body = json.loads(body)
@@ -1523,7 +1600,7 @@ def vdir(obj):
15231600

15241601

15251602
@app.get("/stats", dependencies=[get_rate_limiter(times=2, seconds=2)], response_class=HTMLResponse)
1526-
async def refreshVkey(request: Request, user: schemas.User = Depends(get_current_user_from_token)):
1603+
async def statsg(request: Request, user: schemas.User = Depends(get_current_user_from_token)):
15271604
if not user.isAdmin == True:
15281605
return "You are not authorized to access the library stats page, only Admins can do this."
15291606
try:

ubiblio/vars.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
REDIS_URL = os.environ.get("REDIS_URI", "redis://localhost")
1010

1111
LANGUAGE = os.environ.get("LANGUAGE", "")
12-
12+
GOOGLE_BOOKS_API_KEY = os.environ.get("GB_API", "")
1313
TOKEN_TTL = int(os.environ.get("ACCESS_TOKEN_EXPIRE_MINUTES", "120"))
1414

1515
CREATE_ADMIN_USER = os.environ.get("CREATE_ADMIN_USER", "false").lower() == "true"

0 commit comments

Comments
 (0)