201 lines
7.9 KiB
Python
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")
|