138 lines
4.5 KiB
Python
138 lines
4.5 KiB
Python
"""The left pane for single-stream catalogues: what is on, what is next.
|
|
|
|
A catalogue with one station does not need a station list, but it usually has
|
|
a broadcast schedule. This pane answers the two questions a listener actually
|
|
has — "who is on air?" and "when is the next show?" — in the space the list
|
|
would have used.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, datetime, timedelta
|
|
from typing import TYPE_CHECKING, cast
|
|
|
|
from rich.console import Group, RenderableType
|
|
from rich.text import Text
|
|
from textual.widget import Widget
|
|
|
|
from .common import local_time, local_zone, state_glyph, truncate
|
|
|
|
if TYPE_CHECKING:
|
|
from ..app import BeefyPlayerApp
|
|
from ..models import Broadcast
|
|
|
|
|
|
def _window(broadcast: Broadcast | None) -> str:
|
|
"""``since 22:00 · until 23:00`` — whichever ends the feed actually sent."""
|
|
if broadcast is None:
|
|
return ""
|
|
start = local_time(broadcast.start) if broadcast.start else ""
|
|
end = local_time(broadcast.end) if broadcast.end else ""
|
|
if start and end:
|
|
return f"since {start} · until {end}"
|
|
if start:
|
|
return f"since {start}"
|
|
if end:
|
|
return f"until {end}"
|
|
return ""
|
|
|
|
|
|
def _day_label(day: date, today: date) -> str:
|
|
if day == today:
|
|
return "TODAY"
|
|
if day == today + timedelta(days=1):
|
|
return "TOMORROW"
|
|
return day.strftime("%a %d %b").upper()
|
|
|
|
|
|
def _upcoming_rows(
|
|
upcoming: list[Broadcast], width: int, budget: int
|
|
) -> list[Text]:
|
|
"""Upcoming shows, grouped by local day, clipped to the pane."""
|
|
zone = local_zone()
|
|
today = datetime.now(zone).date()
|
|
|
|
# (is a show, its line) so the "… +N more" count talks about shows and
|
|
# not about the day headings in between.
|
|
rows: list[tuple[bool, Text]] = []
|
|
current_day: date | None = None
|
|
for item in upcoming:
|
|
if item.start is None:
|
|
continue
|
|
day = item.start.astimezone(zone).date()
|
|
if day != current_day:
|
|
current_day = day
|
|
rows.append((False, Text(_day_label(day, today), style="dim")))
|
|
row = Text(no_wrap=True)
|
|
row.append(" ")
|
|
row.append(local_time(item.start), style="bold")
|
|
row.append(" ")
|
|
row.append(truncate(item.display_name, max(4, width - 7)))
|
|
rows.append((True, row))
|
|
|
|
if len(rows) > budget:
|
|
kept = rows[: max(0, budget - 1)]
|
|
while kept and not kept[-1][0]:
|
|
kept.pop()
|
|
dropped = sum(1 for is_show, _ in rows[len(kept) :] if is_show)
|
|
lines = [text for _, text in kept]
|
|
if dropped:
|
|
lines.append(Text(f"… +{dropped} more", style="dim italic"))
|
|
return lines
|
|
return [text for _, text in rows]
|
|
|
|
|
|
class SchedulePanel(Widget):
|
|
can_focus = False
|
|
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.border_title = "SCHEDULE"
|
|
|
|
def render(self) -> RenderableType:
|
|
app = cast("BeefyPlayerApp", self.app)
|
|
width = max(12, self.size.width)
|
|
height = max(1, self.size.height)
|
|
|
|
broadcasts = list(getattr(app, "schedule", ()) or ())
|
|
loaded = bool(getattr(app, "schedule_loaded", False))
|
|
live = next((item for item in broadcasts if item.live), None)
|
|
upcoming = [item for item in broadcasts if not item.live]
|
|
|
|
lines: list[Text] = []
|
|
|
|
head = Text(no_wrap=True)
|
|
glyph, colour = state_glyph("playing" if live else "stopped")
|
|
head.append(f"{glyph} ", style=f"bold {colour}")
|
|
head.append("ON AIR" if live else "AUTO DJ", style="bold magenta")
|
|
if live is not None:
|
|
head.append(" ", style="")
|
|
head.append(
|
|
truncate(live.display_name, max(4, width - 12)),
|
|
style="bold",
|
|
)
|
|
lines.append(head)
|
|
|
|
window = _window(live)
|
|
lines.append(
|
|
Text(truncate(window, width), style="dim")
|
|
if window
|
|
else Text("library rotation", style="dim italic")
|
|
)
|
|
lines.append(Text(""))
|
|
lines.append(Text("UPCOMING", style="bold cyan"))
|
|
|
|
if not loaded:
|
|
lines.append(Text("loading schedule…", style="dim italic"))
|
|
elif not upcoming:
|
|
lines.append(Text("nothing scheduled", style="dim italic"))
|
|
else:
|
|
lines.extend(
|
|
_upcoming_rows(upcoming, width, max(0, height - 1 - len(lines)))
|
|
)
|
|
|
|
lines.append(Text(""))
|
|
while len(lines) < height:
|
|
lines.append(Text(""))
|
|
return Group(*lines[:height])
|