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"]})