import imdb import library_api MEDIA_KINDS = { "show": {"key": "shows", "label": "TV show", "omdb_type": "series"}, "movie": {"key": "movies", "label": "movie", "omdb_type": "movie"}, } STATUSES = ["watched", "watching", "planning"] STATUS_ORDER = {"watching": 0, "watched": 1, "planning": 2} def check_rating(rating: int | None) -> int | None: if rating is not None and not 0 <= rating <= 10: raise ValueError("Rating must be between 0 and 10") return rating def check_status(status: str) -> str: if status not in STATUSES: raise ValueError(f"Status must be one of {', '.join(STATUSES)}") return status def status_of(item: dict) -> str: return item.get("status", "watched") def with_details(item: dict, details: dict) -> dict: return {**item, "imdbId": imdb.imdb_id(item["url"]), "details": details.get(item["url"]) or {}} def list_items(kind: str) -> list[dict]: library = library_api.fetch_library() items = [with_details(item, library["mediaDetails"]) for item in library[MEDIA_KINDS[kind]["key"]]] rating = lambda item: item["rating"] if item.get("rating") is not None else -1 return sorted(items, key=lambda item: (STATUS_ORDER[status_of(item)], -rating(item))) def find_item(items: list[dict], text: str) -> dict | None: identifier = imdb.imdb_id(text) for item in items: if identifier and item["imdbId"] == identifier: return item if item.get("name", "").lower() == text.lower(): return item return None def listed_identifier(kind: str, text: str) -> str: item = find_item(list_items(kind), text) if not item: raise ValueError(f"{text} is not listed") return item["imdbId"] def lookup_details(identifier: str, name: str | None) -> dict: try: return imdb.details(identifier) except ValueError: if name: return {} raise ValueError("Could not look up the title on OMDb. Enter a name, or set OMDB_API_KEY in .env") def add_item(kind: str, text: str, rating: int | None, name: str | None = None, status: str = "watched", collection: str | None = None) -> dict: identifier = imdb.imdb_id(text) if not identifier: raise ValueError(f"{text} is not an IMDb link or ID") check_rating(rating) check_status(status) if find_item(list_items(kind), identifier): raise ValueError(f"{identifier} is already listed") details = lookup_details(identifier, name) fields = { **details, "kind": kind, "name": name or details.get("name") or identifier, "status": status, "rating": rating, "collection": collection or None, } return library_api.save_media(identifier, fields)["item"] def update_item(kind: str, text: str, fields: dict) -> dict: return library_api.save_media(listed_identifier(kind, text), fields)["item"] def remove_item(kind: str, text: str) -> None: library_api.delete_media(listed_identifier(kind, text)) def rate_item(kind: str, text: str, rating: int | None) -> dict: check_rating(rating) return update_item(kind, text, {"rating": rating}) def set_status(kind: str, text: str, status: str) -> dict: check_status(status) return update_item(kind, text, {"status": status}) def set_collection(kind: str, text: str, collection: str | None) -> dict: return update_item(kind, text, {"collection": collection or None}) def set_dates(kind: str, text: str, started: str | None, finished: str | None) -> dict: return update_item(kind, text, {"started": started, "finished": finished}) def has_details(item: dict) -> bool: return any(item["details"].get(key) for key in ["year", "genre", "plot"]) def has_stored_poster(item: dict) -> bool: return library_api.is_stored_image(item["details"].get("poster")) def refresh_details(identifier: str) -> dict: details = {key: value for key, value in imdb.details(identifier).items() if key != "name"} return library_api.save_media(identifier, details)