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
+50
-76
@@ -1,8 +1,6 @@
|
||||
import imdb
|
||||
import jsonfile
|
||||
import media_cache
|
||||
import library_api
|
||||
|
||||
ABOUT_FILE = "about.json"
|
||||
MEDIA_KINDS = {
|
||||
"show": {"key": "shows", "label": "TV show", "omdb_type": "series"},
|
||||
"movie": {"key": "movies", "label": "movie", "omdb_type": "movie"},
|
||||
@@ -23,125 +21,101 @@ def check_status(status: str) -> str:
|
||||
return status
|
||||
|
||||
|
||||
def load_items(kind: str) -> tuple[dict, list[dict]]:
|
||||
data = jsonfile.load(ABOUT_FILE)
|
||||
return data, data.setdefault(MEDIA_KINDS[kind]["key"], [])
|
||||
|
||||
|
||||
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]:
|
||||
_, items = load_items(kind)
|
||||
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 collection_names(kind: str) -> list[str]:
|
||||
_, items = load_items(kind)
|
||||
return sorted({item["collection"] for item in items if item.get("collection")})
|
||||
|
||||
|
||||
def find_index(items: list[dict], text: str) -> int | None:
|
||||
def find_item(items: list[dict], text: str) -> dict | None:
|
||||
identifier = imdb.imdb_id(text)
|
||||
for index, item in enumerate(items):
|
||||
if identifier and imdb.imdb_id(item.get("url", "")) == identifier:
|
||||
return index
|
||||
for item in items:
|
||||
if identifier and item["imdbId"] == identifier:
|
||||
return item
|
||||
if item.get("name", "").lower() == text.lower():
|
||||
return index
|
||||
return item
|
||||
return None
|
||||
|
||||
|
||||
def details_for(identifier: str, name: str | None) -> dict:
|
||||
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 media_cache.cached_details(identifier)
|
||||
except (OSError, ValueError):
|
||||
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 with_optional(item: dict, key: str, value) -> dict:
|
||||
updated = {name: field for name, field in item.items() if name != key}
|
||||
if value is not None and value != "":
|
||||
updated[key] = value
|
||||
return updated
|
||||
|
||||
|
||||
def build_item(name: str, url: str, rating: int | None, status: str, collection: str | None) -> dict:
|
||||
item = {"name": name, "url": url}
|
||||
item = with_optional(item, "rating", rating)
|
||||
if status != "watched":
|
||||
item["status"] = status
|
||||
return with_optional(item, "collection", collection)
|
||||
|
||||
|
||||
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)
|
||||
data, items = load_items(kind)
|
||||
if find_index(items, identifier) is not None:
|
||||
if find_item(list_items(kind), identifier):
|
||||
raise ValueError(f"{identifier} is already listed")
|
||||
details = details_for(identifier, name)
|
||||
item = build_item(name or details.get("title") or identifier, imdb.title_url(identifier), rating, status, collection)
|
||||
items.append(item)
|
||||
jsonfile.save(ABOUT_FILE, data)
|
||||
return item
|
||||
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, change) -> dict:
|
||||
data, items = load_items(kind)
|
||||
index = find_index(items, text)
|
||||
if index is None:
|
||||
raise ValueError(f"{text} is not listed")
|
||||
items[index] = change(items[index])
|
||||
jsonfile.save(ABOUT_FILE, data)
|
||||
return items[index]
|
||||
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) -> dict:
|
||||
data, items = load_items(kind)
|
||||
index = find_index(items, text)
|
||||
if index is None:
|
||||
raise ValueError(f"{text} is not listed")
|
||||
removed = items.pop(index)
|
||||
jsonfile.save(ABOUT_FILE, data)
|
||||
identifier = imdb.imdb_id(removed.get("url", ""))
|
||||
if identifier:
|
||||
media_cache.remove_details(identifier)
|
||||
return removed
|
||||
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, lambda item: with_optional(item, "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, lambda item: with_optional(item, "status", None if status == "watched" else status))
|
||||
return update_item(kind, text, {"status": status})
|
||||
|
||||
|
||||
def set_collection(kind: str, text: str, collection: str | None) -> dict:
|
||||
return update_item(kind, text, lambda item: with_optional(item, "collection", collection))
|
||||
return update_item(kind, text, {"collection": collection or None})
|
||||
|
||||
|
||||
def item_identifiers(kind: str) -> list[str]:
|
||||
_, items = load_items(kind)
|
||||
return [identifier for identifier in (imdb.imdb_id(item.get("url", "")) for item in items) if identifier]
|
||||
def set_dates(kind: str, text: str, started: str | None, finished: str | None) -> dict:
|
||||
return update_item(kind, text, {"started": started, "finished": finished})
|
||||
|
||||
|
||||
def missing_details(kind: str) -> list[str]:
|
||||
return media_cache.missing_identifiers(item_identifiers(kind))
|
||||
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:
|
||||
return media_cache.cached_details(identifier, fresh=True)
|
||||
|
||||
|
||||
def stored_details(identifier: str) -> dict | None:
|
||||
return media_cache.load_details().get(identifier)
|
||||
details = {key: value for key, value in imdb.details(identifier).items() if key != "name"}
|
||||
return library_api.save_media(identifier, details)
|
||||
Reference in new issue
Block a user