Files
website/tools/content/books.py
T

184 lines
5.4 KiB
Python

import datetime
import book_cache
import goodreads
import jsonfile
BOOKS_FILE = "books.json"
SHELVES = ["read", "reading", "want-to-read"]
DESCRIPTION_MAX_LENGTH = 600
GENRE_LIMIT = 5
def check_rating(rating: int) -> int:
if not 0 <= rating <= 10:
raise ValueError("Rating must be between 0 and 10")
return rating
def check_shelf(shelf: str) -> str:
if shelf not in SHELVES:
raise ValueError(f"Shelf must be one of {', '.join(SHELVES)}")
return shelf
def load_books() -> list[dict]:
if not jsonfile.content_path(BOOKS_FILE).exists():
return []
return jsonfile.load(BOOKS_FILE)
def save_books(books: list[dict]) -> None:
jsonfile.save(BOOKS_FILE, books)
def find_index(books: list[dict], text: str) -> int | None:
identifier = goodreads.book_id(text) or (text if text.isdigit() else None)
for index, book in enumerate(books):
if identifier and book.get("goodreadsId") == identifier:
return index
if book.get("title", "").lower() == text.lower():
return index
return None
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:
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"],
"published": scraped["published"],
"genres": scraped["genres"][:GENRE_LIMIT],
"averageRating": scraped["averageRating"],
"ratingCount": scraped["ratingCount"],
}
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 series_tag(series: dict, entry: dict) -> dict:
tag = {"name": series["name"], "url": series["url"]}
if entry.get("position"):
tag["position"] = entry["position"]
return tag
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})
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:
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:
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
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 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)
def move_book(text: str, shelf: str) -> dict:
check_shelf(shelf)
return update_book(text, lambda book: {**book, "shelf": shelf})
def refresh_book(text: str) -> dict:
books = load_books()
index = find_index(books, text)
if index is None:
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)})
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)