131 lines
4.0 KiB
Python
131 lines
4.0 KiB
Python
import book_cache
|
|
import goodreads
|
|
import library_api
|
|
|
|
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]:
|
|
return library_api.fetch_library()["books"]
|
|
|
|
|
|
def find_book(books: list[dict], text: str) -> dict | None:
|
|
identifier = goodreads.book_id(text) or (text if text.isdigit() else None)
|
|
for book in books:
|
|
if identifier and book.get("goodreadsId") == identifier:
|
|
return book
|
|
if book.get("title", "").lower() == text.lower():
|
|
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 scraped_fields(scraped: dict) -> dict:
|
|
return {
|
|
"title": scraped["title"],
|
|
"authors": scraped["authors"],
|
|
"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, fields: dict) -> dict:
|
|
return library_api.save_book(listed_identifier(text), fields)
|
|
|
|
|
|
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)
|
|
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 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_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")
|
|
|
|
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) -> 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)
|
|
return update_book(text, {"rating": rating})
|
|
|
|
|
|
def move_book(text: str, shelf: str) -> dict:
|
|
check_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:
|
|
book = find_book(load_books(), text)
|
|
if not book:
|
|
raise ValueError(f"{text} is not listed")
|
|
scraped = book_cache.scrape_book(book["url"], fresh=True)
|
|
return library_api.save_book(book["goodreadsId"], scraped_fields(scraped))
|
|
|
|
|
|
def store_book_cover(book: dict) -> dict:
|
|
return library_api.save_book(book["goodreadsId"], {"coverSource": book["coverSource"]})
|