Files
website/tools/content/tui/media_pane.py
T

201 lines
7.9 KiB
Python

import time
from rich.text import Text
from textual import work
from textual.app import ComposeResult
from textual.binding import Binding
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
REQUEST_DELAY_SECONDS = 0.5
STATUS_STYLES = {"watching": "blue", "watched": "green", "planning": "magenta"}
def rating_text(item: dict) -> str:
return f"{item['rating']}/10" if item.get("rating") is not None else "-"
class MediaPane(ContentPane):
BINDINGS = [
Binding("a", "add", "Add"),
Binding("e", "rate", "Rate"),
Binding("s", "status", "Status"),
Binding("g", "collection", "Group"),
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"]
def compose(self) -> ComposeResult:
yield DataTable(cursor_type="row", zebra_stripes=True)
def on_mount(self):
table = self.table()
table.add_column("Status", key="status")
table.add_column("Rating", key="rating")
table.add_column("Title", key="title")
table.add_column("Group", key="collection")
table.add_column("Year", key="year")
table.add_column("Cache", key="cache")
table.add_column("IMDb", key="imdb")
self.reload()
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", "")
status = media.status_of(item)
details = stored.get(identifier)
state = cache_state(bool(details), bool(details) and media_cache.has_local_poster(identifier, details))
table.add_row(
Text(status, style=STATUS_STYLES[status]),
rating_text(item),
item.get("name", ""),
item.get("collection", ""),
(details or {}).get("year") or "-",
cache_text(state),
identifier,
key=identifier,
)
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)
def collection_hint(self) -> str:
names = media.collection_names(self.kind)
return f"Existing: {', '.join(names)}" if names else "e.g. Star Wars"
def action_add(self):
fields = [
FormField("link", "IMDb link or ID", placeholder="https://www.imdb.com/title/tt0816692/", required=True),
FormField("status", "Status", kind="select", options=media.STATUSES, value="watched"),
FormField("rating", "Your rating (0 to 10, optional)", kind="rating"),
FormField("collection", "Group (optional, same name groups titles together)", placeholder=self.collection_hint()),
FormField("name", "Name (looked up on OMDb when empty)"),
]
self.app.push_screen(FormDialog(f"Add a {self.label}", fields, "Add"), self.add_item)
def add_item(self, values: dict | None):
if not values:
return
identifier = imdb.imdb_id(values["link"])
def added(item: dict):
self.reload(identifier)
self.notify(f"Added {item['name']} ({media.status_of(item)})")
self.run_in_background(
f"Adding {identifier or values['link']}...",
lambda: media.add_item(self.kind, values["link"], values["rating"], values["name"], values["status"], values["collection"]),
added,
)
def action_rate(self):
item = self.selected_item()
if not item:
return
identifier = imdb.imdb_id(item["url"])
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)]
def rated(values: dict | None):
if values is not None:
self.run_now(lambda: media.rate_item(self.kind, identifier, values["rating"]), f"Updated {item['name']}", identifier)
self.app.push_screen(FormDialog("Change rating", fields), rated)
def action_status(self):
item = self.selected_item()
if not item:
return
identifier = imdb.imdb_id(item["url"])
fields = [FormField("status", f"Status for {item['name']}", kind="select", options=media.STATUSES, value=media.status_of(item))]
def changed(values: dict | None):
if values:
status = values["status"]
self.run_now(lambda: media.set_status(self.kind, identifier, status), f"{item['name']} is now {status}", identifier)
self.app.push_screen(FormDialog("Change status", fields, "Save"), changed)
def action_collection(self):
item = self.selected_item()
if not item:
return
identifier = imdb.imdb_id(item["url"])
fields = [FormField("collection", f"Group for {item['name']} (empty to remove)", value=item.get("collection", ""), placeholder=self.collection_hint())]
def grouped(values: dict | None):
if values is not None:
collection = values["collection"]
message = f"Added {item['name']} to {collection}" if collection else f"Removed {item['name']} from its group"
self.run_now(lambda: media.set_collection(self.kind, identifier, collection), message, identifier)
self.app.push_screen(FormDialog("Group titles", fields), grouped)
def action_delete(self):
identifier = self.selected_key()
if not identifier:
return
name = self.table().get_cell(identifier, "title")
def confirmed(should_delete: bool | None):
if should_delete:
self.run_now(lambda: media.remove_item(self.kind, identifier), f"Removed {name}")
self.app.push_screen(ConfirmDialog(f"Remove {name}?"), confirmed)
def action_refresh(self):
item = self.selected_item()
if not item:
return
identifier = imdb.imdb_id(item["url"])
def refreshed(details: 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)
if not missing:
self.notify(f"Every {self.label} already has saved details and a poster")
return
self.notify(f"Saving details for {len(missing)} titles...")
self.cache_missing_details(missing)
@work(thread=True, exclusive=True, group="media-cache")
def cache_missing_details(self, missing: list[str]):
saved = 0
for index, identifier in enumerate(missing):
if index:
time.sleep(REQUEST_DELAY_SECONDS)
try:
media.refresh_details(identifier)
except (OSError, ValueError) as error:
self.app.call_from_thread(self.notify, f"{identifier}: {error}", title="Skipped", severity="warning")
continue
saved += 1
self.app.call_from_thread(lambda: self.reload(self.selected_key()))
self.app.call_from_thread(self.notify, f"Saved {saved} of {len(missing)} titles")