feat(providers): add a pluggable station-catalogue seam
The app now holds one RadioProvider and asks it for stations, song history and stream URLs instead of importing SomaFM's HTTP client directly. - models.py: provider-neutral Song/Station plus the shared parsers every catalogue needs (Station.provider/.uid keep ids unique once there is more than one). - providers/base.py: the RadioProvider interface and the toolkit it needs in practice - httpx client factory, .pls/.m3u resolution with an 8h disk cache, and a default stream_candidates() strategy. - providers/somafm/: everything SomaFM-specific (channels.json, songs/<id>.json, PLS layout, guessed icecast mirrors) behind SomaFMProvider. - providers/__init__.py: registry, get_provider() and --provider, remembered in the config file. Widgets render branding, splash copy and the help credits from the active provider, so a second catalogue needs no UI changes; with SomaFM selected the screen is byte-identical to before.
This commit is contained in:
1 parent
e30ca09627
commit
c4cc91ee1a
13 files changed
+708
-372
No files matched your search
@@ -0,0 +1,55 @@
|
||||
"""SomaFM: the station catalogue Beefy Player was born with.
|
||||
|
||||
Everything SomaFM-specific lives in this package — the ``channels.json``
|
||||
schema, the ``songs/<station>.json`` history, the ``.pls`` layout and the
|
||||
guessed icecast mirrors. The rest of the app only sees
|
||||
:class:`SomaFMProvider`, which implements
|
||||
:class:`beefy_player.providers.base.RadioProvider`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
|
||||
from ... import config
|
||||
from ...models import Song, Station
|
||||
from ..base import RadioProvider
|
||||
from . import api, fallback
|
||||
|
||||
__all__ = ["SomaFMProvider"]
|
||||
|
||||
|
||||
class SomaFMProvider(RadioProvider):
|
||||
id = "somafm"
|
||||
name = "SomaFM"
|
||||
homepage = "somafm.com"
|
||||
brand = "SOMA FM"
|
||||
tagline = "listener-supported radio · san francisco"
|
||||
station_source = "somafm.com/channels.json"
|
||||
history_source = "songs/<station>.json"
|
||||
|
||||
@property
|
||||
def user_agent(self) -> str:
|
||||
# SomaFM asks clients to identify themselves, so say who we are and
|
||||
# where we come from.
|
||||
return f"{config.USER_AGENT}; +https://somafm.com"
|
||||
|
||||
async def fetch_stations(self, client: httpx.AsyncClient) -> list[Station]:
|
||||
return await api.fetch_channels(client)
|
||||
|
||||
def load_cached_stations(self) -> list[Station]:
|
||||
return api.load_cached_channels()
|
||||
|
||||
def fallback_stations(self) -> list[Station]:
|
||||
return api.parse_channels({"channels": fallback.fallback_channels()})
|
||||
|
||||
async def fetch_history(
|
||||
self, client: httpx.AsyncClient, station_id: str
|
||||
) -> list[Song]:
|
||||
return await api.fetch_history(client, station_id)
|
||||
|
||||
def load_cached_history(self, station_id: str) -> list[Song]:
|
||||
return api.load_cached_songs(station_id)
|
||||
|
||||
def fallback_urls(self, station: Station) -> list[str]:
|
||||
return api.synthetic_mirrors(station.id)
|
||||
Reference in new issue
Block a user