Add SQLite library with API, library desktop apps and MobyGames scraper

This commit is contained in:
SirBlob committed 2026-11-02 23:26:31 -05:00
1 parent 6674428346
commit a16444451a
69 files changed
+2988 -1026

No files matched your search

+34 -28
View File
@@ -8,10 +8,9 @@ from textual.widgets import DataTable
import imdb
import media
import media_cache
from tui.cache_indicator import cache_state, cache_text
from tui.content_pane import ContentPane
from tui.dialogs import ConfirmDialog, FormDialog, FormField
from tui.library_pane import LibraryPane
REQUEST_DELAY_SECONDS = 0.5
STATUS_STYLES = {"watching": "blue", "watched": "green", "planning": "magenta"}
@@ -21,25 +20,25 @@ def rating_text(item: dict) -> str:
return f"{item['rating']}/10" if item.get("rating") is not None else "-"
class MediaPane(ContentPane):
class MediaPane(LibraryPane):
BINDINGS = [
Binding("a", "add", "Add"),
Binding("e", "rate", "Rate"),
Binding("s", "status", "Status"),
Binding("g", "collection", "Group"),
Binding("h", "dates", "Dates"),
Binding("d", "delete", "Delete"),
Binding("u", "refresh", "Refresh"),
Binding("C", "cache_all", "Cache all"),
Binding("r", "reload", "Reload"),
]
content_file = "about.json"
def __init__(self, kind: str, **kwargs):
super().__init__(**kwargs)
self.kind = kind
self.label = media.MEDIA_KINDS[kind]["label"]
self.border_subtitle = media.MEDIA_KINDS[kind]["key"]
self.items: dict[str, dict] = {}
def compose(self) -> ComposeResult:
yield DataTable(cursor_type="row", zebra_stripes=True)
@@ -51,6 +50,7 @@ class MediaPane(ContentPane):
table.add_column("Title", key="title")
table.add_column("Group", key="collection")
table.add_column("Year", key="year")
table.add_column("Finished", key="finished")
table.add_column("Cache", key="cache")
table.add_column("IMDb", key="imdb")
self.reload()
@@ -58,30 +58,29 @@ class MediaPane(ContentPane):
def reload(self, focus_key: str | None = None):
table = self.table()
table.clear()
stored = media_cache.load_details()
for item in media.list_items(self.kind):
identifier = imdb.imdb_id(item.get("url", "")) or item.get("name", "")
items = self.load_entries(lambda: media.list_items(self.kind))
self.items = {item["imdbId"]: item for item in items}
for item in items:
status = media.status_of(item)
details = stored.get(identifier)
state = cache_state(bool(details), bool(details) and media_cache.has_local_poster(identifier, details))
state = cache_state(media.has_details(item), media.has_stored_poster(item))
table.add_row(
Text(status, style=STATUS_STYLES[status]),
rating_text(item),
item.get("name", ""),
item.get("collection", ""),
(details or {}).get("year") or "-",
item["details"].get("year") or "-",
item.get("finished") or "-",
cache_text(state),
identifier,
key=identifier,
item["imdbId"],
key=item["imdbId"],
)
self.restore_cursor(focus_key)
def selected_item(self) -> dict | None:
identifier = self.selected_key()
return next((item for item in media.list_items(self.kind) if imdb.imdb_id(item.get("url", "")) == identifier), None)
return self.items.get(self.selected_key())
def collection_hint(self) -> str:
names = media.collection_names(self.kind)
names = sorted({item["collection"] for item in self.items.values() if item.get("collection")})
return f"Existing: {', '.join(names)}" if names else "e.g. Star Wars"
def action_add(self):
@@ -113,7 +112,7 @@ class MediaPane(ContentPane):
item = self.selected_item()
if not item:
return
identifier = imdb.imdb_id(item["url"])
identifier = item["imdbId"]
current = "" if item.get("rating") is None else str(item["rating"])
fields = [FormField("rating", f"Rating for {item['name']} (empty to clear)", kind="rating", value=current)]
@@ -127,7 +126,7 @@ class MediaPane(ContentPane):
item = self.selected_item()
if not item:
return
identifier = imdb.imdb_id(item["url"])
identifier = item["imdbId"]
fields = [FormField("status", f"Status for {item['name']}", kind="select", options=media.STATUSES, value=media.status_of(item))]
def changed(values: dict | None):
@@ -141,7 +140,7 @@ class MediaPane(ContentPane):
item = self.selected_item()
if not item:
return
identifier = imdb.imdb_id(item["url"])
identifier = item["imdbId"]
fields = [FormField("collection", f"Group for {item['name']} (empty to remove)", value=item.get("collection", ""), placeholder=self.collection_hint())]
def grouped(values: dict | None):
@@ -152,32 +151,39 @@ class MediaPane(ContentPane):
self.app.push_screen(FormDialog("Group titles", fields), grouped)
def action_delete(self):
identifier = self.selected_key()
if not identifier:
def action_dates(self):
item = self.selected_item()
if not item:
return
name = self.table().get_cell(identifier, "title")
identifier = item["imdbId"]
self.edit_dates(item["name"], item, lambda started, finished: media.set_dates(self.kind, identifier, started, finished), identifier)
def action_delete(self):
item = self.selected_item()
if not item:
return
identifier = item["imdbId"]
def confirmed(should_delete: bool | None):
if should_delete:
self.run_now(lambda: media.remove_item(self.kind, identifier), f"Removed {name}")
self.run_now(lambda: media.remove_item(self.kind, identifier), f"Removed {item['name']}")
self.app.push_screen(ConfirmDialog(f"Remove {name}?"), confirmed)
self.app.push_screen(ConfirmDialog(f"Remove {item['name']}?"), confirmed)
def action_refresh(self):
item = self.selected_item()
if not item:
return
identifier = imdb.imdb_id(item["url"])
identifier = item["imdbId"]
def refreshed(details: dict):
def refreshed(_saved: dict):
self.reload(identifier)
self.notify(f"Saved details and poster for {item['name']}")
self.run_in_background(f"Looking up {item['name']} on OMDb...", lambda: media.refresh_details(identifier), refreshed)
def action_cache_all(self):
missing = media.missing_details(self.kind)
missing = [identifier for identifier, item in self.items.items() if not media.has_details(item) or not media.has_stored_poster(item)]
if not missing:
self.notify(f"Every {self.label} already has saved details and a poster")
return