Add SQLite library with API, library desktop apps and MobyGames scraper
This commit is contained in:
1 parent
6674428346
commit
a16444451a
69 files changed
+2988
-1026
No files matched your search
+39
-104
@@ -1,12 +1,7 @@
|
||||
import datetime
|
||||
|
||||
import book_cache
|
||||
import goodreads
|
||||
import jsonfile
|
||||
import library_api
|
||||
|
||||
BOOKS_FILE = "books.json"
|
||||
BOOK_INFO_FILE = "bookDetails.json"
|
||||
PERSONAL_KEYS = ["goodreadsId", "title", "url", "shelf", "rating", "added", "series"]
|
||||
SHELVES = ["read", "reading", "want-to-read"]
|
||||
DESCRIPTION_MAX_LENGTH = 600
|
||||
GENRE_LIMIT = 5
|
||||
@@ -25,55 +20,36 @@ def check_shelf(shelf: str) -> str:
|
||||
|
||||
|
||||
def load_books() -> list[dict]:
|
||||
if not jsonfile.content_path(BOOKS_FILE).exists():
|
||||
return []
|
||||
info = jsonfile.load_data(BOOK_INFO_FILE, {})
|
||||
return [{**info.get(book["goodreadsId"], {}), **book} for book in jsonfile.load(BOOKS_FILE)]
|
||||
return library_api.fetch_library()["books"]
|
||||
|
||||
|
||||
def personal_fields(book: dict) -> dict:
|
||||
return {key: book[key] for key in PERSONAL_KEYS if key in book}
|
||||
|
||||
|
||||
def info_fields(book: dict) -> dict:
|
||||
return {key: value for key, value in book.items() if key not in PERSONAL_KEYS}
|
||||
|
||||
|
||||
def save_books(books: list[dict]) -> None:
|
||||
jsonfile.save(BOOKS_FILE, [personal_fields(book) for book in books])
|
||||
jsonfile.save_data(BOOK_INFO_FILE, {book["goodreadsId"]: info_fields(book) for book in books})
|
||||
|
||||
|
||||
def find_index(books: list[dict], text: str) -> int | None:
|
||||
def find_book(books: list[dict], text: str) -> dict | None:
|
||||
identifier = goodreads.book_id(text) or (text if text.isdigit() else None)
|
||||
for index, book in enumerate(books):
|
||||
for book in books:
|
||||
if identifier and book.get("goodreadsId") == identifier:
|
||||
return index
|
||||
return book
|
||||
if book.get("title", "").lower() == text.lower():
|
||||
return index
|
||||
return book
|
||||
return None
|
||||
|
||||
|
||||
def listed_identifier(text: str) -> str:
|
||||
book = find_book(load_books(), text)
|
||||
if not book:
|
||||
raise ValueError(f"{text} is not listed")
|
||||
return book["goodreadsId"]
|
||||
|
||||
|
||||
def short_description(text: str | None) -> str | None:
|
||||
if not text or len(text) <= DESCRIPTION_MAX_LENGTH:
|
||||
return text
|
||||
return text[:DESCRIPTION_MAX_LENGTH].rsplit(" ", 1)[0] + "..."
|
||||
|
||||
|
||||
def local_cover(scraped: dict) -> str | None:
|
||||
try:
|
||||
return book_cache.cache_cover(scraped["goodreadsId"], scraped["cover"]) or scraped["cover"]
|
||||
except OSError:
|
||||
return scraped["cover"]
|
||||
|
||||
|
||||
def stored_fields(scraped: dict) -> dict:
|
||||
def scraped_fields(scraped: dict) -> dict:
|
||||
return {
|
||||
"goodreadsId": scraped["goodreadsId"],
|
||||
"url": scraped["url"],
|
||||
"title": scraped["title"],
|
||||
"authors": scraped["authors"],
|
||||
"cover": local_cover(scraped),
|
||||
"coverSource": scraped["cover"],
|
||||
"description": short_description(scraped["description"]),
|
||||
"pages": scraped["pages"],
|
||||
@@ -84,14 +60,8 @@ def stored_fields(scraped: dict) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def update_book(text: str, change) -> dict:
|
||||
books = load_books()
|
||||
index = find_index(books, text)
|
||||
if index is None:
|
||||
raise ValueError(f"{text} is not listed")
|
||||
books[index] = change(books[index])
|
||||
save_books(books)
|
||||
return books[index]
|
||||
def update_book(text: str, fields: dict) -> dict:
|
||||
return library_api.save_book(listed_identifier(text), fields)
|
||||
|
||||
|
||||
def series_tag(series: dict, entry: dict) -> dict:
|
||||
@@ -104,92 +74,57 @@ def series_tag(series: dict, entry: dict) -> dict:
|
||||
def add_series_book(series: dict, entry: dict, shelf: str) -> dict:
|
||||
check_shelf(shelf)
|
||||
tag = series_tag(series, entry)
|
||||
books = load_books()
|
||||
index = find_index(books, entry["goodreadsId"])
|
||||
if index is not None:
|
||||
books[index] = {**books[index], "shelf": shelf, "series": tag}
|
||||
save_books(books)
|
||||
return books[index]
|
||||
book = add_book(entry["url"], shelf)
|
||||
return update_book(book["goodreadsId"], lambda stored: {**stored, "series": tag})
|
||||
if find_book(load_books(), entry["goodreadsId"]):
|
||||
return library_api.save_book(entry["goodreadsId"], {"shelf": shelf, "series": tag})
|
||||
return add_book(entry["url"], shelf, series=tag)
|
||||
|
||||
|
||||
def library_shelves() -> dict[str, str]:
|
||||
return {book["goodreadsId"]: book.get("shelf", "read") for book in load_books()}
|
||||
|
||||
|
||||
def add_book(url: str, shelf: str = "read", rating: int | None = None) -> dict:
|
||||
def add_book(url: str, shelf: str = "read", rating: int | None = None, series: dict | None = None) -> dict:
|
||||
check_shelf(shelf)
|
||||
if rating is not None:
|
||||
check_rating(rating)
|
||||
identifier = goodreads.book_id(url)
|
||||
if not identifier:
|
||||
raise ValueError(f"{url} is not a Goodreads book link")
|
||||
if find_index(load_books(), identifier) is not None:
|
||||
if find_book(load_books(), identifier):
|
||||
raise ValueError(f"{identifier} is already listed")
|
||||
|
||||
scraped = book_cache.scrape_book(url)
|
||||
if not scraped["title"]:
|
||||
raise ValueError("Could not read the book from Goodreads")
|
||||
|
||||
book = stored_fields(scraped)
|
||||
book["shelf"] = shelf
|
||||
if rating is not None:
|
||||
book["rating"] = rating
|
||||
book["added"] = datetime.date.today().isoformat()
|
||||
|
||||
books = load_books()
|
||||
books.append(book)
|
||||
save_books(books)
|
||||
return book
|
||||
fields = {**scraped_fields(scraped), "shelf": shelf, "rating": rating}
|
||||
if series:
|
||||
fields["series"] = series
|
||||
return library_api.save_book(identifier, fields)
|
||||
|
||||
|
||||
def remove_book(text: str) -> dict:
|
||||
books = load_books()
|
||||
index = find_index(books, text)
|
||||
if index is None:
|
||||
raise ValueError(f"{text} is not listed")
|
||||
removed = books.pop(index)
|
||||
save_books(books)
|
||||
book_cache.remove_cover(removed["goodreadsId"])
|
||||
return removed
|
||||
def remove_book(text: str) -> None:
|
||||
library_api.delete_book(listed_identifier(text))
|
||||
|
||||
|
||||
def rate_book(text: str, rating: int | None) -> dict:
|
||||
if rating is not None:
|
||||
check_rating(rating)
|
||||
|
||||
def change(book: dict) -> dict:
|
||||
updated = {key: value for key, value in book.items() if key != "rating"}
|
||||
if rating is not None:
|
||||
updated["rating"] = rating
|
||||
return updated
|
||||
|
||||
return update_book(text, change)
|
||||
return update_book(text, {"rating": rating})
|
||||
|
||||
|
||||
def move_book(text: str, shelf: str) -> dict:
|
||||
check_shelf(shelf)
|
||||
return update_book(text, lambda book: {**book, "shelf": shelf})
|
||||
return update_book(text, {"shelf": shelf})
|
||||
|
||||
|
||||
def set_dates(text: str, started: str | None, finished: str | None) -> dict:
|
||||
return update_book(text, {"started": started, "finished": finished})
|
||||
|
||||
|
||||
def refresh_book(text: str) -> dict:
|
||||
books = load_books()
|
||||
index = find_index(books, text)
|
||||
if index is None:
|
||||
book = find_book(load_books(), text)
|
||||
if not book:
|
||||
raise ValueError(f"{text} is not listed")
|
||||
scraped = book_cache.scrape_book(books[index]["url"], fresh=True)
|
||||
return update_book(text, lambda book: {**book, **stored_fields(scraped)})
|
||||
scraped = book_cache.scrape_book(book["url"], fresh=True)
|
||||
return library_api.save_book(book["goodreadsId"], scraped_fields(scraped))
|
||||
|
||||
|
||||
def books_missing_local_covers() -> list[dict]:
|
||||
return [book for book in load_books() if not book_cache.is_local_cover(book.get("cover"))]
|
||||
|
||||
|
||||
def cache_book_cover(identifier: str) -> dict:
|
||||
def change(book: dict) -> dict:
|
||||
source = book.get("coverSource") or book.get("cover")
|
||||
cover = book_cache.cache_cover(identifier, source)
|
||||
return {**book, "cover": cover or book.get("cover"), "coverSource": source}
|
||||
|
||||
return update_book(identifier, change)
|
||||
def store_book_cover(book: dict) -> dict:
|
||||
return library_api.save_book(book["goodreadsId"], {"coverSource": book["coverSource"]})
|
||||
Reference in new issue
Block a user