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

207 lines
8.2 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
from tui.cache_indicator import cache_state, cache_text
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"}
def rating_text(item: dict) -> str:
return f"{item['rating']}/10" if item.get("rating") is not None else "-"
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"),
]
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)
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("Finished", key="finished")
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()
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)
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", ""),
item["details"].get("year") or "-",
item.get("finished") or "-",
cache_text(state),
item["imdbId"],
key=item["imdbId"],
)
self.restore_cursor(focus_key)
def selected_item(self) -> dict | None:
return self.items.get(self.selected_key())
def collection_hint(self) -> str:
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):
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 = 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)]
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 = 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):
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 = 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):
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_dates(self):
item = self.selected_item()
if not item:
return
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 {item['name']}")
self.app.push_screen(ConfirmDialog(f"Remove {item['name']}?"), confirmed)
def action_refresh(self):
item = self.selected_item()
if not item:
return
identifier = item["imdbId"]
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 = [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
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")