feat(ui): add catalogue picker, schedule pane and per-provider hero

This commit is contained in:
beefcarver committed 2026-11-03 17:51:27 -05:00
1 parent 058d336a2c
commit b31ad3f6fb
16 files changed
+1211 -65

No files matched your search

+2 -1
View File
@@ -18,6 +18,7 @@ from __future__ import annotations
from .base import RadioProvider
from .somafm import SomaFMProvider
from .tilderadio import TilderadioProvider
__all__ = [
"DEFAULT_PROVIDER",
@@ -29,7 +30,7 @@ __all__ = [
#: Every catalogue the app can talk to, keyed by its slug.
PROVIDERS: dict[str, RadioProvider] = {
provider.id: provider for provider in (SomaFMProvider(),)
provider.id: provider for provider in (SomaFMProvider(), TilderadioProvider())
}
#: Provider used when nothing else is configured.
+33 -1
View File
@@ -24,7 +24,7 @@ from urllib.parse import quote
import httpx
from .. import config
from ..models import Song, Station
from ..models import Broadcast, Song, Station
__all__ = [
"PLAYLIST_TTL",
@@ -92,6 +92,28 @@ class RadioProvider(ABC):
#: ``User-Agent`` for this catalogue's requests (SomaFM likes to know).
user_agent: str = ""
# -- header hero ----------------------------------------------------- #
#: Rows of header art drawn at the top left (rich markup is allowed).
hero: tuple[str, ...] = ()
#: Paint :attr:`hero` with the app's spectrum gradient (a wordmark) instead
#: of the colours the art carries itself (a picture).
hero_gradient: bool = False
#: Short lines printed beside the hero, top to bottom.
hero_lines: tuple[str, ...] = ()
# -- left pane ------------------------------------------------------- #
#: Show a broadcast schedule in the left pane instead of a station list.
#: Set by single-stream catalogues, where a list of one station is a waste
#: of the pane and "what is on, what is next" is the useful view.
has_schedule: bool = False
#: Look the current track up on Bandcamp and offer a link when it is found.
#: Off for catalogues whose rotation is unlikely to be there (and for
#: stations that would rather not be scraped).
bandcamp: bool = True
# -- catalogue ------------------------------------------------------- #
@abstractmethod
@@ -118,6 +140,16 @@ class RadioProvider(ABC):
def load_cached_history(self, station_id: str) -> list[Song]:
"""The last good song history from disk, or ``[]``."""
# -- schedule -------------------------------------------------------- #
async def fetch_schedule(self, client: httpx.AsyncClient) -> list[Broadcast]:
"""Upcoming broadcasts, soonest first (the live one, if any, first)."""
return []
def load_cached_schedule(self) -> list[Broadcast]:
"""The last good schedule from disk, or ``[]``."""
return []
# -- streams --------------------------------------------------------- #
def fallback_urls(self, station: Station) -> list[str]:
@@ -15,6 +15,7 @@ from ... import config
from ...models import Song, Station
from ..base import RadioProvider
from . import api, fallback
from . import hero as hero_art
__all__ = ["SomaFMProvider"]
@@ -28,6 +29,10 @@ class SomaFMProvider(RadioProvider):
station_source = "somafm.com/channels.json"
history_source = "songs/<station>.json"
hero = hero_art.LOGO
hero_gradient = True
hero_lines = hero_art.TAGLINES
@property
def user_agent(self) -> str:
# SomaFM asks clients to identify themselves, so say who we are and
+20
View File
@@ -0,0 +1,20 @@
"""SomaFM's header hero: the block-letter wordmark and the three lines beside it.
The glyphs are plain half-blocks; the header paints them with the app's
spectrum gradient (``hero_gradient``), so the logo takes on the terminal's own
colours instead of carrying RGB values around.
"""
from __future__ import annotations
LOGO: tuple[str, ...] = (
"▄▀▀▀ ▄▀▀▄ █▄ ▄█ ▄▀▀▄",
"▀▀▀▄ █ █ █ █ █ █▀▀█",
"▄▄▄▀ ▀▄▄▀ █ █ █ █",
)
TAGLINES: tuple[str, ...] = (
"SOMA FM RADIO",
"LISTENER SUPPORTED",
"SAN FRANCISCO",
)